@vibe-forge/workspace-assets 0.8.0 → 0.8.4
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/AGENTS.md +7 -2
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +845 -0
- package/__tests__/adapter-asset-plan.spec.ts +170 -0
- package/__tests__/bundle.spec.ts +212 -0
- package/__tests__/prompt-selection.spec.ts +388 -0
- package/__tests__/snapshot.ts +276 -0
- package/__tests__/test-helpers.ts +33 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +204 -0
- package/package.json +5 -5
- package/src/index.ts +1368 -3
- package/src/internal-types.ts +1 -39
- package/__tests__/workspace-assets.spec.ts +0 -279
- package/src/adapter-asset-plan.ts +0 -174
- package/src/bundle.ts +0 -185
- package/src/document-assets.ts +0 -201
- package/src/helpers.ts +0 -35
- package/src/plugin-assets.ts +0 -178
- package/src/prompt-selection.ts +0 -151
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { resolvePromptAssetSelection, resolveWorkspaceAssetBundle } from '#~/index.js'
|
|
6
|
+
|
|
7
|
+
import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
|
|
8
|
+
|
|
9
|
+
describe('resolvePromptAssetSelection', () => {
|
|
10
|
+
it('embeds only alwaysApply rules and keeps optional rules as summaries', async () => {
|
|
11
|
+
const workspace = await createWorkspace()
|
|
12
|
+
|
|
13
|
+
await writeDocument(
|
|
14
|
+
join(workspace, '.ai/rules/base.md'),
|
|
15
|
+
[
|
|
16
|
+
'---',
|
|
17
|
+
'alwaysApply: true',
|
|
18
|
+
'description: 基础约束',
|
|
19
|
+
'---',
|
|
20
|
+
'始终检查公共边界。'
|
|
21
|
+
].join('\n')
|
|
22
|
+
)
|
|
23
|
+
await writeDocument(
|
|
24
|
+
join(workspace, '.ai/rules/optional.md'),
|
|
25
|
+
[
|
|
26
|
+
'---',
|
|
27
|
+
'alwaysApply: false',
|
|
28
|
+
'description: 按需参考规则',
|
|
29
|
+
'---',
|
|
30
|
+
'只有在特定场景才需要展开。'
|
|
31
|
+
].join('\n')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
35
|
+
cwd: workspace,
|
|
36
|
+
useDefaultVibeForgeMcpServer: false
|
|
37
|
+
})
|
|
38
|
+
const [, options] = await resolvePromptAssetSelection({
|
|
39
|
+
bundle,
|
|
40
|
+
type: undefined
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(options.systemPrompt).toContain('# base')
|
|
44
|
+
expect(options.systemPrompt).toContain('> 始终检查公共边界。')
|
|
45
|
+
expect(options.systemPrompt).toContain('# optional')
|
|
46
|
+
expect(options.systemPrompt).toContain('> 适用场景:按需参考规则')
|
|
47
|
+
expect(options.systemPrompt).toContain('> 规则文件路径:.ai/rules/optional.md')
|
|
48
|
+
expect(options.systemPrompt).toContain('> 仅在任务满足上述场景时,再阅读该规则文件。')
|
|
49
|
+
expect(options.systemPrompt).not.toContain('> 只有在特定场景才需要展开。')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('selects local assets by short name and scoped plugin assets explicitly', async () => {
|
|
53
|
+
const workspace = await createWorkspace()
|
|
54
|
+
|
|
55
|
+
await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
|
|
56
|
+
'package.json': JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
name: '@vibe-forge/plugin-demo',
|
|
59
|
+
version: '1.0.0'
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
2
|
|
63
|
+
),
|
|
64
|
+
'specs/release/index.md': '---\ndescription: 插件发布流程\n---\n执行插件发布'
|
|
65
|
+
})
|
|
66
|
+
await writeDocument(
|
|
67
|
+
join(workspace, '.ai/specs/release.md'),
|
|
68
|
+
'---\ndescription: 项目发布流程\n---\n执行项目发布'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
72
|
+
cwd: workspace,
|
|
73
|
+
configs: [{
|
|
74
|
+
plugins: [
|
|
75
|
+
{ id: 'demo', scope: 'demo' }
|
|
76
|
+
]
|
|
77
|
+
}, undefined],
|
|
78
|
+
useDefaultVibeForgeMcpServer: false
|
|
79
|
+
})
|
|
80
|
+
const [localData, localOptions] = await resolvePromptAssetSelection({
|
|
81
|
+
bundle,
|
|
82
|
+
type: 'spec',
|
|
83
|
+
name: 'release'
|
|
84
|
+
})
|
|
85
|
+
const [pluginData, pluginOptions] = await resolvePromptAssetSelection({
|
|
86
|
+
bundle,
|
|
87
|
+
type: 'spec',
|
|
88
|
+
name: 'demo/release'
|
|
89
|
+
})
|
|
90
|
+
const localSpecId = bundle.specs.find(asset => asset.origin === 'workspace' && asset.name === 'release')?.id
|
|
91
|
+
const pluginSpecId = bundle.specs.find(asset => asset.origin === 'plugin' && asset.displayName === 'demo/release')
|
|
92
|
+
?.id
|
|
93
|
+
|
|
94
|
+
expect(localData.targetBody).toContain('执行项目发布')
|
|
95
|
+
expect(localData.targetBody).not.toContain('执行插件发布')
|
|
96
|
+
expect(localOptions.systemPrompt).toContain('项目发布流程')
|
|
97
|
+
expect(localOptions.systemPrompt).toContain('demo/release')
|
|
98
|
+
expect(localOptions.promptAssetIds).toEqual(expect.arrayContaining([localSpecId]))
|
|
99
|
+
|
|
100
|
+
expect(pluginData.targetBody).toContain('执行插件发布')
|
|
101
|
+
expect(pluginOptions.systemPrompt).toContain('插件发布流程')
|
|
102
|
+
expect(pluginOptions.promptAssetIds).toEqual(expect.arrayContaining([pluginSpecId]))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('formats rules as markdown headings and blockquotes', async () => {
|
|
106
|
+
const workspace = await createWorkspace()
|
|
107
|
+
|
|
108
|
+
await writeDocument(
|
|
109
|
+
join(workspace, '.ai/rules/required.md'),
|
|
110
|
+
[
|
|
111
|
+
'---',
|
|
112
|
+
'description: 必须执行的规则',
|
|
113
|
+
'alwaysApply: true',
|
|
114
|
+
'---',
|
|
115
|
+
'# 标题',
|
|
116
|
+
'',
|
|
117
|
+
'正文第一行',
|
|
118
|
+
'正文第二行'
|
|
119
|
+
].join('\n')
|
|
120
|
+
)
|
|
121
|
+
await writeDocument(
|
|
122
|
+
join(workspace, '.ai/rules/summary-only.md'),
|
|
123
|
+
[
|
|
124
|
+
'---',
|
|
125
|
+
'description: 只展示摘要',
|
|
126
|
+
'alwaysApply: false',
|
|
127
|
+
'---',
|
|
128
|
+
'不应该出现在引用正文里'
|
|
129
|
+
].join('\n')
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
133
|
+
cwd: workspace,
|
|
134
|
+
configs: [undefined, undefined],
|
|
135
|
+
useDefaultVibeForgeMcpServer: false
|
|
136
|
+
})
|
|
137
|
+
const [, resolvedOptions] = await resolvePromptAssetSelection({
|
|
138
|
+
bundle,
|
|
139
|
+
type: undefined
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(resolvedOptions.systemPrompt).toContain('# required')
|
|
143
|
+
expect(resolvedOptions.systemPrompt).toContain('> # 标题')
|
|
144
|
+
expect(resolvedOptions.systemPrompt).toContain('> 正文第一行')
|
|
145
|
+
expect(resolvedOptions.systemPrompt).toContain('> 正文第二行')
|
|
146
|
+
expect(resolvedOptions.systemPrompt).toContain('# summary-only')
|
|
147
|
+
expect(resolvedOptions.systemPrompt).toContain('> 适用场景:只展示摘要')
|
|
148
|
+
expect(resolvedOptions.systemPrompt).toContain('> 规则文件路径:.ai/rules/summary-only.md')
|
|
149
|
+
expect(resolvedOptions.systemPrompt).not.toContain('> 不应该出现在引用正文里')
|
|
150
|
+
expect(resolvedOptions.systemPrompt).not.toContain('--------------------')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('keeps skills as route-only guidance unless the target spec references them', async () => {
|
|
154
|
+
const workspace = await createWorkspace()
|
|
155
|
+
|
|
156
|
+
await writeDocument(
|
|
157
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
158
|
+
[
|
|
159
|
+
'---',
|
|
160
|
+
'description: 检索项目信息',
|
|
161
|
+
'---',
|
|
162
|
+
'先读 README.md'
|
|
163
|
+
].join('\n')
|
|
164
|
+
)
|
|
165
|
+
await writeDocument(
|
|
166
|
+
join(workspace, '.ai/skills/review/SKILL.md'),
|
|
167
|
+
[
|
|
168
|
+
'---',
|
|
169
|
+
'description: 评审代码改动',
|
|
170
|
+
'---',
|
|
171
|
+
'检查回归风险'
|
|
172
|
+
].join('\n')
|
|
173
|
+
)
|
|
174
|
+
await writeDocument(
|
|
175
|
+
join(workspace, '.ai/specs/release/index.md'),
|
|
176
|
+
[
|
|
177
|
+
'---',
|
|
178
|
+
'description: 发布流程',
|
|
179
|
+
'skills:',
|
|
180
|
+
' - research',
|
|
181
|
+
'---',
|
|
182
|
+
'执行发布'
|
|
183
|
+
].join('\n')
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
187
|
+
cwd: workspace,
|
|
188
|
+
useDefaultVibeForgeMcpServer: false
|
|
189
|
+
})
|
|
190
|
+
const [data, options] = await resolvePromptAssetSelection({
|
|
191
|
+
bundle,
|
|
192
|
+
type: 'spec',
|
|
193
|
+
name: 'release'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(data.targetSkills.map(skill => skill.resolvedName ?? skill.attributes.name)).toEqual(['research'])
|
|
197
|
+
expect(options.systemPrompt).toContain('项目已加载如下技能模块')
|
|
198
|
+
expect(options.systemPrompt).toContain('# research')
|
|
199
|
+
expect(options.systemPrompt).toContain('> 技能文件路径:.ai/skills/research/SKILL.md')
|
|
200
|
+
expect(options.systemPrompt).toContain('<skill-content>')
|
|
201
|
+
expect(options.systemPrompt).toContain('先读 README.md')
|
|
202
|
+
expect(options.systemPrompt).toContain('# review')
|
|
203
|
+
expect(options.systemPrompt).toContain('> 技能文件路径:.ai/skills/review/SKILL.md')
|
|
204
|
+
expect(options.systemPrompt).toContain('> 默认无需预先加载正文;仅在任务明确需要该技能时,再读取对应技能文件。')
|
|
205
|
+
expect(options.systemPrompt).not.toContain('<skill-content>\n检查回归风险\n</skill-content>')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('keeps skills as route-only guidance in normal mode', async () => {
|
|
209
|
+
const workspace = await createWorkspace()
|
|
210
|
+
|
|
211
|
+
await writeDocument(
|
|
212
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
213
|
+
[
|
|
214
|
+
'---',
|
|
215
|
+
'description: 检索项目信息',
|
|
216
|
+
'---',
|
|
217
|
+
'先读 README.md'
|
|
218
|
+
].join('\n')
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
222
|
+
cwd: workspace,
|
|
223
|
+
useDefaultVibeForgeMcpServer: false
|
|
224
|
+
})
|
|
225
|
+
const [data, options] = await resolvePromptAssetSelection({
|
|
226
|
+
bundle,
|
|
227
|
+
type: undefined
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
expect(data.targetSkills).toEqual([])
|
|
231
|
+
expect(options.systemPrompt).not.toContain('项目已加载如下技能模块')
|
|
232
|
+
expect(options.systemPrompt).toContain('<skills>')
|
|
233
|
+
expect(options.systemPrompt).toContain('# research')
|
|
234
|
+
expect(options.systemPrompt).toContain('> 技能文件路径:.ai/skills/research/SKILL.md')
|
|
235
|
+
expect(options.systemPrompt).toContain('> 默认无需预先加载正文;仅在任务明确需要该技能时,再读取对应技能文件。')
|
|
236
|
+
expect(options.systemPrompt).not.toContain('<skill-content>')
|
|
237
|
+
expect(options.systemPrompt).not.toContain('先读 README.md')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('keeps spec route guidance without default identity in normal mode', async () => {
|
|
241
|
+
const workspace = await createWorkspace()
|
|
242
|
+
|
|
243
|
+
await writeDocument(
|
|
244
|
+
join(workspace, '.ai/specs/release/index.md'),
|
|
245
|
+
[
|
|
246
|
+
'---',
|
|
247
|
+
'description: 发布流程',
|
|
248
|
+
'---',
|
|
249
|
+
'执行发布'
|
|
250
|
+
].join('\n')
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
254
|
+
cwd: workspace,
|
|
255
|
+
useDefaultVibeForgeMcpServer: false
|
|
256
|
+
})
|
|
257
|
+
const [, options] = await resolvePromptAssetSelection({
|
|
258
|
+
bundle,
|
|
259
|
+
type: undefined
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect(options.systemPrompt).toContain('项目存在如下工作流程')
|
|
263
|
+
expect(options.systemPrompt).toContain('流程名称:release')
|
|
264
|
+
expect(options.systemPrompt).not.toContain('项目推进管理大师')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('injects spec identity guidance when a spec is actively selected', async () => {
|
|
268
|
+
const workspace = await createWorkspace()
|
|
269
|
+
|
|
270
|
+
await writeDocument(
|
|
271
|
+
join(workspace, '.ai/specs/release/index.md'),
|
|
272
|
+
[
|
|
273
|
+
'---',
|
|
274
|
+
'description: 发布流程',
|
|
275
|
+
'---',
|
|
276
|
+
'执行发布'
|
|
277
|
+
].join('\n')
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
281
|
+
cwd: workspace,
|
|
282
|
+
useDefaultVibeForgeMcpServer: false
|
|
283
|
+
})
|
|
284
|
+
const [, options] = await resolvePromptAssetSelection({
|
|
285
|
+
bundle,
|
|
286
|
+
type: 'spec',
|
|
287
|
+
name: 'release'
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
expect(options.systemPrompt).toContain('项目推进管理大师')
|
|
291
|
+
expect(options.systemPrompt).toContain('永远不要单独完成代码开发工作')
|
|
292
|
+
expect(options.systemPrompt).toContain('流程名称:release')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('embeds referenced skills for entity mode and removes them from route guidance', async () => {
|
|
296
|
+
const workspace = await createWorkspace()
|
|
297
|
+
|
|
298
|
+
await writeDocument(
|
|
299
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
300
|
+
[
|
|
301
|
+
'---',
|
|
302
|
+
'description: 检索项目信息',
|
|
303
|
+
'---',
|
|
304
|
+
'先读 README.md'
|
|
305
|
+
].join('\n')
|
|
306
|
+
)
|
|
307
|
+
await writeDocument(
|
|
308
|
+
join(workspace, '.ai/skills/review/SKILL.md'),
|
|
309
|
+
[
|
|
310
|
+
'---',
|
|
311
|
+
'description: 评审代码改动',
|
|
312
|
+
'---',
|
|
313
|
+
'检查回归风险'
|
|
314
|
+
].join('\n')
|
|
315
|
+
)
|
|
316
|
+
await writeDocument(
|
|
317
|
+
join(workspace, '.ai/entities/reviewer/README.md'),
|
|
318
|
+
[
|
|
319
|
+
'---',
|
|
320
|
+
'description: 代码评审实体',
|
|
321
|
+
'skills:',
|
|
322
|
+
' - review',
|
|
323
|
+
'---',
|
|
324
|
+
'负责代码评审'
|
|
325
|
+
].join('\n')
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
329
|
+
cwd: workspace,
|
|
330
|
+
useDefaultVibeForgeMcpServer: false
|
|
331
|
+
})
|
|
332
|
+
const [data, options] = await resolvePromptAssetSelection({
|
|
333
|
+
bundle,
|
|
334
|
+
type: 'entity',
|
|
335
|
+
name: 'reviewer'
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
expect(data.targetSkills.map(skill => skill.resolvedName ?? skill.attributes.name)).toEqual(['review'])
|
|
339
|
+
expect(options.systemPrompt).toContain('项目已加载如下技能模块')
|
|
340
|
+
expect(options.systemPrompt).toContain('# review')
|
|
341
|
+
expect(options.systemPrompt).toContain('<skill-content>')
|
|
342
|
+
expect(options.systemPrompt).toContain('检查回归风险')
|
|
343
|
+
expect(options.systemPrompt).not.toContain('<skills>\n# review')
|
|
344
|
+
expect(options.systemPrompt).toContain('<skills>')
|
|
345
|
+
expect(options.systemPrompt).toContain('# research')
|
|
346
|
+
expect(options.systemPrompt).toContain('> 技能文件路径:.ai/skills/research/SKILL.md')
|
|
347
|
+
expect(options.systemPrompt).not.toContain('<skill-content>\n先读 README.md\n</skill-content>')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('does not preload all skills when the target entity omits skill references', async () => {
|
|
351
|
+
const workspace = await createWorkspace()
|
|
352
|
+
|
|
353
|
+
await writeDocument(
|
|
354
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
355
|
+
[
|
|
356
|
+
'---',
|
|
357
|
+
'description: 检索项目信息',
|
|
358
|
+
'---',
|
|
359
|
+
'先读 README.md'
|
|
360
|
+
].join('\n')
|
|
361
|
+
)
|
|
362
|
+
await writeDocument(
|
|
363
|
+
join(workspace, '.ai/entities/reviewer/README.md'),
|
|
364
|
+
[
|
|
365
|
+
'---',
|
|
366
|
+
'description: 代码评审实体',
|
|
367
|
+
'---',
|
|
368
|
+
'负责代码评审'
|
|
369
|
+
].join('\n')
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
373
|
+
cwd: workspace,
|
|
374
|
+
useDefaultVibeForgeMcpServer: false
|
|
375
|
+
})
|
|
376
|
+
const [data, options] = await resolvePromptAssetSelection({
|
|
377
|
+
bundle,
|
|
378
|
+
type: 'entity',
|
|
379
|
+
name: 'reviewer'
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(data.targetSkills).toEqual([])
|
|
383
|
+
expect(options.systemPrompt).not.toContain('项目已加载如下技能模块')
|
|
384
|
+
expect(options.systemPrompt).toContain('# research')
|
|
385
|
+
expect(options.systemPrompt).toContain('> 技能文件路径:.ai/skills/research/SKILL.md')
|
|
386
|
+
expect(options.systemPrompt).not.toContain('先读 README.md')
|
|
387
|
+
})
|
|
388
|
+
})
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdapterAssetPlan,
|
|
5
|
+
AssetDiagnostic,
|
|
6
|
+
Definition,
|
|
7
|
+
Entity,
|
|
8
|
+
Filter,
|
|
9
|
+
PromptAssetResolution,
|
|
10
|
+
Rule,
|
|
11
|
+
Skill,
|
|
12
|
+
Spec,
|
|
13
|
+
WorkspaceAsset,
|
|
14
|
+
WorkspaceAssetBundle,
|
|
15
|
+
WorkspaceAssetKind
|
|
16
|
+
} from '@vibe-forge/types'
|
|
17
|
+
import { resolveDocumentName, resolveSpecIdentifier } from '@vibe-forge/utils'
|
|
18
|
+
|
|
19
|
+
const sortStrings = (values: string[]) => [...values].sort((left, right) => left.localeCompare(right))
|
|
20
|
+
|
|
21
|
+
const sanitizeValue = (
|
|
22
|
+
value: string,
|
|
23
|
+
cwd: string
|
|
24
|
+
) => (
|
|
25
|
+
value
|
|
26
|
+
.replaceAll(`/private${cwd}`, '<workspace>')
|
|
27
|
+
.replaceAll(cwd, '<workspace>')
|
|
28
|
+
.replaceAll(`/private${process.execPath}`, '<node-path>')
|
|
29
|
+
.replaceAll(process.execPath, '<node-path>')
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const normalizeValue = (
|
|
33
|
+
value: unknown,
|
|
34
|
+
cwd: string
|
|
35
|
+
): unknown => {
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
return sanitizeValue(value, cwd)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map(item => normalizeValue(item, cwd))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value != null && typeof value === 'object') {
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(value)
|
|
47
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
48
|
+
.map(([key, entry]) => [key, normalizeValue(entry, cwd)])
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sortFilter = (filter: Filter | undefined) => {
|
|
56
|
+
if (filter == null) return undefined
|
|
57
|
+
return {
|
|
58
|
+
include: filter.include == null ? undefined : sortStrings(filter.include),
|
|
59
|
+
exclude: filter.exclude == null ? undefined : sortStrings(filter.exclude)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolveDefinitionIdentifier = (
|
|
64
|
+
kind: Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>,
|
|
65
|
+
definition: Definition<Rule | Spec | Entity | Skill>
|
|
66
|
+
) => {
|
|
67
|
+
if (kind === 'spec') {
|
|
68
|
+
return resolveSpecIdentifier(definition.path, definition.attributes.name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (kind === 'entity') {
|
|
72
|
+
return resolveDocumentName(definition.path, definition.attributes.name, ['readme.md', 'index.json'])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (kind === 'skill') {
|
|
76
|
+
return resolveDocumentName(definition.path, definition.attributes.name, ['skill.md'])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return resolveDocumentName(definition.path, definition.attributes.name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const summarizeDefinition = (
|
|
83
|
+
kind: Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>,
|
|
84
|
+
definition: Definition<Rule | Spec | Entity | Skill>,
|
|
85
|
+
cwd: string
|
|
86
|
+
) => ({
|
|
87
|
+
identifier: resolveDefinitionIdentifier(kind, definition),
|
|
88
|
+
path: sanitizeValue(definition.path, cwd),
|
|
89
|
+
attributes: normalizeValue(definition.attributes, cwd),
|
|
90
|
+
body: definition.body.trim()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const buildSnapshotAssetId = (
|
|
94
|
+
asset: WorkspaceAsset,
|
|
95
|
+
cwd: string
|
|
96
|
+
) => (
|
|
97
|
+
`${asset.kind}:${asset.origin}:${asset.instancePath ?? 'workspace'}:${asset.displayName}:${sanitizeValue(asset.sourcePath, cwd)}`
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const summarizeBaseAsset = (
|
|
101
|
+
asset: WorkspaceAsset,
|
|
102
|
+
cwd: string,
|
|
103
|
+
assetIdMap?: Map<string, string>
|
|
104
|
+
) => ({
|
|
105
|
+
id: assetIdMap?.get(asset.id) ?? buildSnapshotAssetId(asset, cwd),
|
|
106
|
+
kind: asset.kind,
|
|
107
|
+
name: asset.name,
|
|
108
|
+
displayName: asset.displayName,
|
|
109
|
+
origin: asset.origin,
|
|
110
|
+
scope: asset.scope,
|
|
111
|
+
sourcePath: sanitizeValue(asset.sourcePath, cwd),
|
|
112
|
+
instancePath: asset.instancePath,
|
|
113
|
+
packageId: asset.packageId,
|
|
114
|
+
resolvedBy: asset.resolvedBy,
|
|
115
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const summarizeDocumentAsset = (
|
|
119
|
+
asset: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
|
|
120
|
+
cwd: string,
|
|
121
|
+
assetIdMap?: Map<string, string>
|
|
122
|
+
) => ({
|
|
123
|
+
...summarizeBaseAsset(asset, cwd, assetIdMap),
|
|
124
|
+
definition: summarizeDefinition(asset.kind, asset.payload.definition, cwd)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const summarizeMcpServer = (
|
|
128
|
+
asset: Extract<WorkspaceAsset, { kind: 'mcpServer' }>,
|
|
129
|
+
cwd: string,
|
|
130
|
+
assetIdMap?: Map<string, string>
|
|
131
|
+
) => ({
|
|
132
|
+
...summarizeBaseAsset(asset, cwd, assetIdMap),
|
|
133
|
+
config: normalizeValue(asset.payload.config, cwd)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const summarizeHookPlugin = (
|
|
137
|
+
asset: Extract<WorkspaceAsset, { kind: 'hookPlugin' }>,
|
|
138
|
+
cwd: string,
|
|
139
|
+
assetIdMap?: Map<string, string>
|
|
140
|
+
) => ({
|
|
141
|
+
...summarizeBaseAsset(asset, cwd, assetIdMap),
|
|
142
|
+
packageName: asset.payload.packageName,
|
|
143
|
+
config: normalizeValue(asset.payload.config, cwd)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const summarizeOpenCodeOverlayAsset = (
|
|
147
|
+
asset: Extract<WorkspaceAsset, { kind: 'agent' | 'command' | 'mode' | 'nativePlugin' }>,
|
|
148
|
+
cwd: string,
|
|
149
|
+
assetIdMap?: Map<string, string>
|
|
150
|
+
) => ({
|
|
151
|
+
...summarizeBaseAsset(asset, cwd, assetIdMap),
|
|
152
|
+
entryName: asset.payload.entryName,
|
|
153
|
+
targetSubpath: asset.payload.targetSubpath
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const summarizeDiagnostics = (
|
|
157
|
+
diagnostics: AssetDiagnostic[],
|
|
158
|
+
assetIdMap: Map<string, string>
|
|
159
|
+
) => (
|
|
160
|
+
[...diagnostics]
|
|
161
|
+
.sort((left, right) => {
|
|
162
|
+
const assetIdDiff = left.assetId.localeCompare(right.assetId)
|
|
163
|
+
if (assetIdDiff !== 0) return assetIdDiff
|
|
164
|
+
return left.status.localeCompare(right.status)
|
|
165
|
+
})
|
|
166
|
+
.map(diagnostic => ({
|
|
167
|
+
assetId: assetIdMap.get(diagnostic.assetId) ?? diagnostic.assetId,
|
|
168
|
+
adapter: diagnostic.adapter,
|
|
169
|
+
status: diagnostic.status,
|
|
170
|
+
reason: diagnostic.reason,
|
|
171
|
+
scope: diagnostic.scope,
|
|
172
|
+
packageId: diagnostic.packageId
|
|
173
|
+
}))
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const summarizePlan = (
|
|
177
|
+
plan: AdapterAssetPlan,
|
|
178
|
+
cwd: string,
|
|
179
|
+
assetIdMap: Map<string, string>
|
|
180
|
+
) => ({
|
|
181
|
+
adapter: plan.adapter,
|
|
182
|
+
mcpServers: normalizeValue(plan.mcpServers, cwd),
|
|
183
|
+
overlays: [...plan.overlays]
|
|
184
|
+
.sort((left, right) => left.assetId.localeCompare(right.assetId))
|
|
185
|
+
.map(entry => ({
|
|
186
|
+
assetId: assetIdMap.get(entry.assetId) ?? entry.assetId,
|
|
187
|
+
kind: entry.kind,
|
|
188
|
+
sourcePath: sanitizeValue(entry.sourcePath, cwd),
|
|
189
|
+
targetPath: entry.targetPath
|
|
190
|
+
})),
|
|
191
|
+
diagnostics: summarizeDiagnostics(plan.diagnostics, assetIdMap)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const summarizeSelection = (
|
|
195
|
+
resolution: PromptAssetResolution,
|
|
196
|
+
options: {
|
|
197
|
+
systemPrompt?: string
|
|
198
|
+
tools?: Filter
|
|
199
|
+
mcpServers?: Filter
|
|
200
|
+
promptAssetIds?: string[]
|
|
201
|
+
},
|
|
202
|
+
cwd: string,
|
|
203
|
+
assetIdMap: Map<string, string>
|
|
204
|
+
) => ({
|
|
205
|
+
resolution: {
|
|
206
|
+
rules: resolution.rules.map(rule => summarizeDefinition('rule', rule, cwd)),
|
|
207
|
+
targetSkills: resolution.targetSkills.map(skill => summarizeDefinition('skill', skill, cwd)),
|
|
208
|
+
entities: resolution.entities.map(entity => summarizeDefinition('entity', entity, cwd)),
|
|
209
|
+
skills: resolution.skills.map(skill => summarizeDefinition('skill', skill, cwd)),
|
|
210
|
+
specs: resolution.specs.map(spec => summarizeDefinition('spec', spec, cwd)),
|
|
211
|
+
targetBody: resolution.targetBody.trim(),
|
|
212
|
+
promptAssetIds: sortStrings(resolution.promptAssetIds.map(assetId => assetIdMap.get(assetId) ?? assetId))
|
|
213
|
+
},
|
|
214
|
+
options: {
|
|
215
|
+
systemPrompt: options.systemPrompt?.trim(),
|
|
216
|
+
tools: sortFilter(options.tools),
|
|
217
|
+
mcpServers: sortFilter(options.mcpServers),
|
|
218
|
+
promptAssetIds: options.promptAssetIds == null
|
|
219
|
+
? undefined
|
|
220
|
+
: sortStrings(options.promptAssetIds.map(assetId => assetIdMap.get(assetId) ?? assetId))
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
export const serializeWorkspaceAssetsSnapshot = (params: {
|
|
225
|
+
cwd: string
|
|
226
|
+
bundle: WorkspaceAssetBundle
|
|
227
|
+
selection: {
|
|
228
|
+
resolution: PromptAssetResolution
|
|
229
|
+
options: {
|
|
230
|
+
systemPrompt?: string
|
|
231
|
+
tools?: Filter
|
|
232
|
+
mcpServers?: Filter
|
|
233
|
+
promptAssetIds?: string[]
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
plans: AdapterAssetPlan[]
|
|
237
|
+
}) => {
|
|
238
|
+
const { bundle, cwd } = params
|
|
239
|
+
const assetIdMap = new Map(bundle.assets.map(asset => [asset.id, buildSnapshotAssetId(asset, cwd)]))
|
|
240
|
+
|
|
241
|
+
const snapshot = {
|
|
242
|
+
bundle: {
|
|
243
|
+
cwd: '<workspace>',
|
|
244
|
+
pluginConfigs: normalizeValue(bundle.pluginConfigs, cwd),
|
|
245
|
+
pluginInstances: normalizeValue(bundle.pluginInstances, cwd),
|
|
246
|
+
defaultIncludeMcpServers: sortStrings(bundle.defaultIncludeMcpServers),
|
|
247
|
+
defaultExcludeMcpServers: sortStrings(bundle.defaultExcludeMcpServers),
|
|
248
|
+
rules: bundle.rules.map(rule => summarizeDocumentAsset(rule, cwd, assetIdMap)),
|
|
249
|
+
specs: bundle.specs.map(spec => summarizeDocumentAsset(spec, cwd, assetIdMap)),
|
|
250
|
+
entities: bundle.entities.map(entity => summarizeDocumentAsset(entity, cwd, assetIdMap)),
|
|
251
|
+
skills: bundle.skills.map(skill => summarizeDocumentAsset(skill, cwd, assetIdMap)),
|
|
252
|
+
mcpServers: Object.values(bundle.mcpServers)
|
|
253
|
+
.sort((left, right) => left.payload.name.localeCompare(right.payload.name))
|
|
254
|
+
.map(server => summarizeMcpServer(server, cwd, assetIdMap)),
|
|
255
|
+
hookPlugins: bundle.hookPlugins
|
|
256
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
257
|
+
.map(plugin => summarizeHookPlugin(plugin, cwd, assetIdMap)),
|
|
258
|
+
opencodeOverlayAssets: bundle.opencodeOverlayAssets
|
|
259
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
260
|
+
.map(asset => summarizeOpenCodeOverlayAsset(asset, cwd, assetIdMap))
|
|
261
|
+
},
|
|
262
|
+
selection: summarizeSelection(
|
|
263
|
+
params.selection.resolution,
|
|
264
|
+
params.selection.options,
|
|
265
|
+
cwd,
|
|
266
|
+
assetIdMap
|
|
267
|
+
),
|
|
268
|
+
plans: Object.fromEntries(
|
|
269
|
+
[...params.plans]
|
|
270
|
+
.sort((left, right) => left.adapter.localeCompare(right.adapter))
|
|
271
|
+
.map(plan => [plan.adapter, summarizePlan(plan, cwd, assetIdMap)])
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`
|
|
276
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { afterEach } from 'vitest'
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
export const createWorkspace = async () => {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), 'workspace-assets-'))
|
|
11
|
+
tempDirs.push(dir)
|
|
12
|
+
return dir
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const writeDocument = async (filePath: string, content: string) => {
|
|
16
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
17
|
+
await writeFile(filePath, content)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const installPluginPackage = async (
|
|
21
|
+
workspace: string,
|
|
22
|
+
packageName: string,
|
|
23
|
+
files: Record<string, string>
|
|
24
|
+
) => {
|
|
25
|
+
const packageDir = join(workspace, 'node_modules', ...packageName.split('/'))
|
|
26
|
+
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
27
|
+
await writeDocument(join(packageDir, relativePath), content)
|
|
28
|
+
}))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
|
33
|
+
})
|