@vibe-forge/workspace-assets 1.0.0 → 2.0.1
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/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +55 -0
- package/__tests__/adapter-asset-plan.spec.ts +220 -6
- package/__tests__/bundle.spec.ts +677 -2
- package/__tests__/prompt-selection.spec.ts +307 -0
- package/__tests__/skill-dependencies-cli.spec.ts +175 -0
- package/__tests__/snapshot.ts +1 -0
- package/__tests__/test-helpers.ts +9 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +2 -2
- package/package.json +5 -5
- package/src/adapter-asset-plan.ts +42 -66
- package/src/asset-source.ts +13 -0
- package/src/bundle-internal.ts +242 -22
- package/src/bundle.ts +4 -0
- package/src/configured-skills.ts +99 -0
- package/src/home-bridge.ts +1 -0
- package/src/index.ts +3 -0
- package/src/prompt-selection.ts +44 -19
- package/src/selection-internal.ts +335 -1
- package/src/skill-dependencies.ts +361 -0
- package/src/skills-cli-dependency.ts +208 -0
- package/src/workspace-config.ts +132 -0
- package/src/workspace-prompt.ts +29 -0
- package/src/workspaces.ts +188 -0
package/__tests__/bundle.spec.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
+
/* eslint-disable import/first, max-lines -- bundle coverage keeps related fixture scenarios in one file */
|
|
1
2
|
import { join } from 'node:path'
|
|
2
3
|
import process from 'node:process'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
const skillsCliMocks = vi.hoisted(() => ({
|
|
9
|
+
findSkillsCli: vi.fn(),
|
|
10
|
+
installSkillsCliRefToTemp: vi.fn(),
|
|
11
|
+
installSkillsCliSkillToTemp: vi.fn()
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('@vibe-forge/utils/skills-cli', async () => {
|
|
15
|
+
const actual = await vi.importActual<typeof import('@vibe-forge/utils/skills-cli')>('@vibe-forge/utils/skills-cli')
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
findSkillsCli: skillsCliMocks.findSkillsCli,
|
|
19
|
+
installSkillsCliRefToTemp: skillsCliMocks.installSkillsCliRefToTemp,
|
|
20
|
+
installSkillsCliSkillToTemp: skillsCliMocks.installSkillsCliSkillToTemp
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
|
|
7
25
|
|
|
8
26
|
import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
|
|
9
27
|
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.clearAllMocks()
|
|
30
|
+
vi.unstubAllGlobals()
|
|
31
|
+
})
|
|
32
|
+
|
|
10
33
|
describe('resolveWorkspaceAssetBundle', () => {
|
|
11
34
|
it('loads npm plugin assets via the package-id fallback and exposes OpenCode overlays', async () => {
|
|
12
35
|
const workspace = await createWorkspace()
|
|
@@ -97,6 +120,620 @@ describe('resolveWorkspaceAssetBundle', () => {
|
|
|
97
120
|
}
|
|
98
121
|
})
|
|
99
122
|
|
|
123
|
+
it('loads local and dev rule files as workspace rules', async () => {
|
|
124
|
+
const workspace = await createWorkspace()
|
|
125
|
+
|
|
126
|
+
await writeDocument(
|
|
127
|
+
join(workspace, '.ai/rules/team.md'),
|
|
128
|
+
'---\ndescription: 团队规则\n---\n团队共享约束'
|
|
129
|
+
)
|
|
130
|
+
await writeDocument(
|
|
131
|
+
join(workspace, '.ai/rules/preference.local.md'),
|
|
132
|
+
'---\ndescription: 本地偏好\nalwaysApply: true\n---\n使用当前用户偏好的输出风格'
|
|
133
|
+
)
|
|
134
|
+
await writeDocument(
|
|
135
|
+
join(workspace, '.ai/rules/debug.dev.md'),
|
|
136
|
+
'---\ndescription: 本地调试\nalwaysApply: true\n---\n优先保留调试证据'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
140
|
+
cwd: workspace,
|
|
141
|
+
configs: [undefined, undefined],
|
|
142
|
+
useDefaultVibeForgeMcpServer: false
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(bundle.rules.map(asset => asset.displayName).sort()).toEqual(['debug.dev', 'preference.local', 'team'])
|
|
146
|
+
expect(bundle.rules.find(asset => asset.displayName === 'preference.local')?.payload.definition.body)
|
|
147
|
+
.toContain('当前用户偏好')
|
|
148
|
+
expect(bundle.rules.find(asset => asset.displayName === 'debug.dev')?.payload.definition.attributes.alwaysApply)
|
|
149
|
+
.toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('bridges supported home skill roots by default and keeps the first duplicate root', async () => {
|
|
153
|
+
const workspace = await createWorkspace()
|
|
154
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
155
|
+
|
|
156
|
+
await writeDocument(
|
|
157
|
+
join(realHome!, '.agents/skills/research/SKILL.md'),
|
|
158
|
+
'---\ndescription: 来自 agents root\n---\n阅读 README.md'
|
|
159
|
+
)
|
|
160
|
+
await writeDocument(
|
|
161
|
+
join(realHome!, '.claude/skills/research/SKILL.md'),
|
|
162
|
+
'---\ndescription: 来自 claude root\n---\n这份定义应被后面的 root 覆盖掉'
|
|
163
|
+
)
|
|
164
|
+
await writeDocument(
|
|
165
|
+
join(realHome!, '.config/opencode/skills/release/SKILL.md'),
|
|
166
|
+
'---\ndescription: 来自 opencode root\n---\n整理发布材料'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
170
|
+
cwd: workspace,
|
|
171
|
+
configs: [undefined, undefined],
|
|
172
|
+
useDefaultVibeForgeMcpServer: false
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(bundle.skills.map(asset => asset.displayName)).toEqual(['research', 'release'])
|
|
176
|
+
expect(bundle.skills.find(asset => asset.name === 'research')).toEqual(expect.objectContaining({
|
|
177
|
+
origin: 'workspace',
|
|
178
|
+
resolvedBy: 'home-bridge',
|
|
179
|
+
sourcePath: join(realHome!, '.agents/skills/research/SKILL.md')
|
|
180
|
+
}))
|
|
181
|
+
expect(bundle.skills.find(asset => asset.name === 'release')).toEqual(expect.objectContaining({
|
|
182
|
+
origin: 'workspace',
|
|
183
|
+
resolvedBy: 'home-bridge',
|
|
184
|
+
sourcePath: join(realHome!, '.config/opencode/skills/release/SKILL.md')
|
|
185
|
+
}))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('can disable the home skill bridge entirely', async () => {
|
|
189
|
+
const workspace = await createWorkspace()
|
|
190
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
191
|
+
|
|
192
|
+
await writeDocument(
|
|
193
|
+
join(realHome!, '.agents/skills/research/SKILL.md'),
|
|
194
|
+
'---\ndescription: 检索资料\n---\n阅读 README.md'
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
198
|
+
cwd: workspace,
|
|
199
|
+
configs: [{
|
|
200
|
+
skills: {
|
|
201
|
+
homeBridge: {
|
|
202
|
+
enabled: false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, undefined],
|
|
206
|
+
useDefaultVibeForgeMcpServer: false
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
expect(bundle.skills).toEqual([])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('supports custom home skill roots with tilde expansion', async () => {
|
|
213
|
+
const workspace = await createWorkspace()
|
|
214
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
215
|
+
|
|
216
|
+
await writeDocument(
|
|
217
|
+
join(realHome!, '.agents/skills/ignored/SKILL.md'),
|
|
218
|
+
'---\ndescription: 默认目录\n---\n这份定义不应被加载'
|
|
219
|
+
)
|
|
220
|
+
await writeDocument(
|
|
221
|
+
join(realHome!, 'custom-skills/writer/SKILL.md'),
|
|
222
|
+
'---\ndescription: 自定义目录\n---\n产出说明文档'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
226
|
+
cwd: workspace,
|
|
227
|
+
configs: [{
|
|
228
|
+
skills: {
|
|
229
|
+
homeBridge: {
|
|
230
|
+
roots: '~/custom-skills'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}, undefined],
|
|
234
|
+
useDefaultVibeForgeMcpServer: false
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
expect(bundle.skills.map(asset => asset.displayName)).toEqual(['writer'])
|
|
238
|
+
expect(bundle.skills[0]?.sourcePath).toBe(join(realHome!, 'custom-skills/writer/SKILL.md'))
|
|
239
|
+
expect(bundle.skills[0]?.resolvedBy).toBe('home-bridge')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('keeps the first matching skill when multiple homeBridge roots contain the same name', async () => {
|
|
243
|
+
const workspace = await createWorkspace()
|
|
244
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
245
|
+
|
|
246
|
+
await writeDocument(
|
|
247
|
+
join(realHome!, '.claude/skills/research/SKILL.md'),
|
|
248
|
+
'---\ndescription: 来自 claude root\n---\n优先保留这份定义'
|
|
249
|
+
)
|
|
250
|
+
await writeDocument(
|
|
251
|
+
join(realHome!, '.agents/skills/research/SKILL.md'),
|
|
252
|
+
'---\ndescription: 来自 agents root\n---\n这份定义应被后面的 root 跳过'
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
256
|
+
cwd: workspace,
|
|
257
|
+
configs: [{
|
|
258
|
+
skills: {
|
|
259
|
+
homeBridge: {
|
|
260
|
+
roots: ['~/.claude/skills', '~/.agents/skills']
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}, undefined],
|
|
264
|
+
useDefaultVibeForgeMcpServer: false
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(bundle.skills.map(asset => asset.displayName)).toEqual(['research'])
|
|
268
|
+
expect(bundle.skills[0]).toEqual(expect.objectContaining({
|
|
269
|
+
origin: 'workspace',
|
|
270
|
+
resolvedBy: 'home-bridge',
|
|
271
|
+
sourcePath: join(realHome!, '.claude/skills/research/SKILL.md')
|
|
272
|
+
}))
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('warns once when a custom homeBridge root uses an unsupported relative path', async () => {
|
|
276
|
+
const workspace = await createWorkspace()
|
|
277
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
281
|
+
cwd: workspace,
|
|
282
|
+
configs: [{
|
|
283
|
+
skills: {
|
|
284
|
+
homeBridge: {
|
|
285
|
+
roots: ['./team-skills']
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}, undefined],
|
|
289
|
+
useDefaultVibeForgeMcpServer: false
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
expect(bundle.skills).toEqual([])
|
|
293
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
294
|
+
expect.stringContaining('Ignoring invalid skills.homeBridge root "./team-skills"')
|
|
295
|
+
)
|
|
296
|
+
} finally {
|
|
297
|
+
warnSpy.mockRestore()
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('lets project and plugin skills override matching home-bridged skills', async () => {
|
|
302
|
+
const workspace = await createWorkspace()
|
|
303
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
304
|
+
|
|
305
|
+
await writeDocument(
|
|
306
|
+
join(realHome!, '.agents/skills/research/SKILL.md'),
|
|
307
|
+
'---\ndescription: home research\n---\nhome research body'
|
|
308
|
+
)
|
|
309
|
+
await writeDocument(
|
|
310
|
+
join(realHome!, '.agents/skills/review/SKILL.md'),
|
|
311
|
+
'---\ndescription: home review\n---\nhome review body'
|
|
312
|
+
)
|
|
313
|
+
await writeDocument(
|
|
314
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
315
|
+
'---\ndescription: project research\n---\nproject research body'
|
|
316
|
+
)
|
|
317
|
+
await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
|
|
318
|
+
'package.json': JSON.stringify(
|
|
319
|
+
{
|
|
320
|
+
name: '@vibe-forge/plugin-review',
|
|
321
|
+
version: '1.0.0'
|
|
322
|
+
},
|
|
323
|
+
null,
|
|
324
|
+
2
|
|
325
|
+
),
|
|
326
|
+
'skills/review/SKILL.md': '---\ndescription: plugin review\n---\nplugin review body'
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
330
|
+
cwd: workspace,
|
|
331
|
+
configs: [{
|
|
332
|
+
plugins: [
|
|
333
|
+
{ id: 'review' }
|
|
334
|
+
]
|
|
335
|
+
}, undefined],
|
|
336
|
+
useDefaultVibeForgeMcpServer: false
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual(['research', 'review'])
|
|
340
|
+
expect(bundle.skills.find(asset => asset.name === 'research')).toEqual(expect.objectContaining({
|
|
341
|
+
sourcePath: join(workspace, '.ai/skills/research/SKILL.md'),
|
|
342
|
+
resolvedBy: undefined
|
|
343
|
+
}))
|
|
344
|
+
expect(bundle.skills.find(asset => asset.name === 'review')).toEqual(expect.objectContaining({
|
|
345
|
+
origin: 'plugin',
|
|
346
|
+
sourcePath: expect.stringContaining('/node_modules/@vibe-forge/plugin-review/skills/review/SKILL.md')
|
|
347
|
+
}))
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('keeps scoped project skills alongside unscoped home skills with the same base name', async () => {
|
|
351
|
+
const workspace = await createWorkspace()
|
|
352
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
353
|
+
|
|
354
|
+
await writeDocument(
|
|
355
|
+
join(realHome!, '.agents/skills/research/SKILL.md'),
|
|
356
|
+
'---\ndescription: home research\n---\nhome research body'
|
|
357
|
+
)
|
|
358
|
+
await installPluginPackage(workspace, '@vibe-forge/plugin-team', {
|
|
359
|
+
'package.json': JSON.stringify(
|
|
360
|
+
{
|
|
361
|
+
name: '@vibe-forge/plugin-team',
|
|
362
|
+
version: '1.0.0'
|
|
363
|
+
},
|
|
364
|
+
null,
|
|
365
|
+
2
|
|
366
|
+
),
|
|
367
|
+
'skills/research/SKILL.md': '---\ndescription: scoped research\n---\nscoped research body'
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
371
|
+
cwd: workspace,
|
|
372
|
+
configs: [{
|
|
373
|
+
plugins: [
|
|
374
|
+
{ id: 'team', scope: 'team' }
|
|
375
|
+
]
|
|
376
|
+
}, undefined],
|
|
377
|
+
useDefaultVibeForgeMcpServer: false
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual(['research', 'team/research'])
|
|
381
|
+
expect(bundle.skills.find(asset => asset.displayName === 'research')).toEqual(expect.objectContaining({
|
|
382
|
+
resolvedBy: 'home-bridge'
|
|
383
|
+
}))
|
|
384
|
+
expect(bundle.skills.find(asset => asset.displayName === 'team/research')).toEqual(expect.objectContaining({
|
|
385
|
+
origin: 'plugin'
|
|
386
|
+
}))
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('installs selected missing skill dependencies from the skills CLI cache', async () => {
|
|
390
|
+
const workspace = await createWorkspace()
|
|
391
|
+
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
392
|
+
const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
|
|
393
|
+
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
|
|
394
|
+
await writeDocument(
|
|
395
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
396
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
|
|
397
|
+
)
|
|
398
|
+
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
399
|
+
tempDir: tempInstallDir,
|
|
400
|
+
installedSkill: {
|
|
401
|
+
dirName: 'frontend-design',
|
|
402
|
+
name: 'frontend-design',
|
|
403
|
+
sourcePath: installedSkillDir
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
await writeDocument(
|
|
408
|
+
join(realHome!, '.agents/skills/frontend-design/SKILL.md'),
|
|
409
|
+
'---\ndescription: home frontend design\n---\nUse the home definition.'
|
|
410
|
+
)
|
|
411
|
+
await writeDocument(
|
|
412
|
+
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
413
|
+
[
|
|
414
|
+
'---',
|
|
415
|
+
'name: app-builder',
|
|
416
|
+
'description: Build apps',
|
|
417
|
+
'dependencies:',
|
|
418
|
+
' - anthropics/skills@frontend-design',
|
|
419
|
+
'---',
|
|
420
|
+
'Build the app.'
|
|
421
|
+
].join('\n')
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
425
|
+
cwd: workspace,
|
|
426
|
+
configs: [undefined, undefined],
|
|
427
|
+
useDefaultVibeForgeMcpServer: false
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
431
|
+
expect(bundle.skills.find(asset => asset.name === 'frontend-design')).toEqual(expect.objectContaining({
|
|
432
|
+
resolvedBy: 'home-bridge',
|
|
433
|
+
sourcePath: join(realHome!, '.agents/skills/frontend-design/SKILL.md')
|
|
434
|
+
}))
|
|
435
|
+
|
|
436
|
+
await buildAdapterAssetPlan({
|
|
437
|
+
adapter: 'opencode',
|
|
438
|
+
bundle,
|
|
439
|
+
options: {
|
|
440
|
+
skills: {
|
|
441
|
+
include: ['app-builder']
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
|
|
447
|
+
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
448
|
+
expect(dependency?.sourcePath).toContain(
|
|
449
|
+
'/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
|
|
450
|
+
)
|
|
451
|
+
expect(bundle.skills.find(asset => (
|
|
452
|
+
asset.name === 'frontend-design' && asset.resolvedBy === 'home-bridge'
|
|
453
|
+
))).toBeUndefined()
|
|
454
|
+
expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
|
|
455
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
456
|
+
config: undefined,
|
|
457
|
+
skill: 'frontend-design',
|
|
458
|
+
source: 'anthropics/skills'
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('installs configured project skills before bundle resolution and rewrites renamed skill names', async () => {
|
|
463
|
+
const workspace = await createWorkspace()
|
|
464
|
+
const tempInstallDir = join(workspace, '.tmp-configured-install')
|
|
465
|
+
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
|
|
466
|
+
await writeDocument(
|
|
467
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
468
|
+
'---\nname: design-review\ndescription: Review design work\n---\nReview the UI implementation.\n'
|
|
469
|
+
)
|
|
470
|
+
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
471
|
+
tempDir: tempInstallDir,
|
|
472
|
+
installedSkill: {
|
|
473
|
+
dirName: 'design-review',
|
|
474
|
+
name: 'design-review',
|
|
475
|
+
sourcePath: installedSkillDir
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
480
|
+
cwd: workspace,
|
|
481
|
+
configs: [{
|
|
482
|
+
skills: [
|
|
483
|
+
{
|
|
484
|
+
name: 'design-review',
|
|
485
|
+
source: 'example-source/default/public',
|
|
486
|
+
rename: 'internal-review'
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
}, undefined],
|
|
490
|
+
syncConfiguredSkills: true,
|
|
491
|
+
useDefaultVibeForgeMcpServer: false
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
expect(bundle.skills.map(asset => asset.name)).toContain('internal-review')
|
|
495
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
496
|
+
config: undefined,
|
|
497
|
+
skill: 'design-review',
|
|
498
|
+
source: 'example-source/default/public'
|
|
499
|
+
})
|
|
500
|
+
await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
|
|
501
|
+
'name: internal-review'
|
|
502
|
+
)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('skips configured skill reinstalls unless updateConfiguredSkills is enabled', async () => {
|
|
506
|
+
const workspace = await createWorkspace()
|
|
507
|
+
await writeDocument(
|
|
508
|
+
join(workspace, '.ai/skills/internal-review/SKILL.md'),
|
|
509
|
+
'---\nname: internal-review\ndescription: Existing skill\n---\nExisting content.\n'
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
const skippedBundle = await resolveWorkspaceAssetBundle({
|
|
513
|
+
cwd: workspace,
|
|
514
|
+
configs: [{
|
|
515
|
+
skills: [
|
|
516
|
+
{
|
|
517
|
+
name: 'design-review',
|
|
518
|
+
source: 'example-source/default/public',
|
|
519
|
+
rename: 'internal-review'
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
}, undefined],
|
|
523
|
+
syncConfiguredSkills: true,
|
|
524
|
+
useDefaultVibeForgeMcpServer: false
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
expect(skippedBundle.skills.map(asset => asset.name)).toContain('internal-review')
|
|
528
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
529
|
+
|
|
530
|
+
const tempInstallDir = join(workspace, '.tmp-configured-update')
|
|
531
|
+
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
|
|
532
|
+
await writeDocument(
|
|
533
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
534
|
+
'---\nname: design-review\ndescription: Updated skill\n---\nUpdated content.\n'
|
|
535
|
+
)
|
|
536
|
+
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValueOnce({
|
|
537
|
+
tempDir: tempInstallDir,
|
|
538
|
+
installedSkill: {
|
|
539
|
+
dirName: 'design-review',
|
|
540
|
+
name: 'design-review',
|
|
541
|
+
sourcePath: installedSkillDir
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
await resolveWorkspaceAssetBundle({
|
|
546
|
+
cwd: workspace,
|
|
547
|
+
configs: [{
|
|
548
|
+
skills: [
|
|
549
|
+
{
|
|
550
|
+
name: 'design-review',
|
|
551
|
+
source: 'example-source/default/public',
|
|
552
|
+
rename: 'internal-review'
|
|
553
|
+
}
|
|
554
|
+
]
|
|
555
|
+
}, undefined],
|
|
556
|
+
syncConfiguredSkills: true,
|
|
557
|
+
updateConfiguredSkills: true,
|
|
558
|
+
useDefaultVibeForgeMcpServer: false
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledTimes(1)
|
|
562
|
+
await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
|
|
563
|
+
'Updated content.'
|
|
564
|
+
)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('installs skill dependencies into the primary workspace shared cache', async () => {
|
|
568
|
+
const primary = await createWorkspace()
|
|
569
|
+
const worktree = await createWorkspace()
|
|
570
|
+
const previousPrimaryWorkspace = process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
|
|
571
|
+
const tempInstallDir = join(worktree, '.tmp-install-skills-cli')
|
|
572
|
+
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
|
|
573
|
+
await writeDocument(
|
|
574
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
575
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse primary cache.\n'
|
|
576
|
+
)
|
|
577
|
+
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
578
|
+
tempDir: tempInstallDir,
|
|
579
|
+
installedSkill: {
|
|
580
|
+
dirName: 'frontend-design',
|
|
581
|
+
name: 'frontend-design',
|
|
582
|
+
sourcePath: installedSkillDir
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = primary
|
|
588
|
+
await writeDocument(
|
|
589
|
+
join(worktree, '.ai/skills/app-builder/SKILL.md'),
|
|
590
|
+
[
|
|
591
|
+
'---',
|
|
592
|
+
'name: app-builder',
|
|
593
|
+
'description: Build apps',
|
|
594
|
+
'dependencies:',
|
|
595
|
+
' - anthropics/skills@frontend-design',
|
|
596
|
+
'---',
|
|
597
|
+
'Build the app.'
|
|
598
|
+
].join('\n')
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
602
|
+
cwd: worktree,
|
|
603
|
+
configs: [undefined, undefined],
|
|
604
|
+
useDefaultVibeForgeMcpServer: false
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
await buildAdapterAssetPlan({
|
|
608
|
+
adapter: 'opencode',
|
|
609
|
+
bundle,
|
|
610
|
+
options: {
|
|
611
|
+
skills: {
|
|
612
|
+
include: ['app-builder']
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
|
|
618
|
+
expect(dependency?.sourcePath).toContain(join(
|
|
619
|
+
primary,
|
|
620
|
+
'.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
|
|
621
|
+
))
|
|
622
|
+
expect(dependency?.sourcePath).not.toContain(join(worktree, '.ai/caches'))
|
|
623
|
+
} finally {
|
|
624
|
+
if (previousPrimaryWorkspace == null) {
|
|
625
|
+
delete process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
|
|
626
|
+
} else {
|
|
627
|
+
process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = previousPrimaryWorkspace
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it('reuses complete skill dependency caches without deleting or downloading them again', async () => {
|
|
633
|
+
const workspace = await createWorkspace()
|
|
634
|
+
|
|
635
|
+
const cachedSkillPath = join(
|
|
636
|
+
workspace,
|
|
637
|
+
'.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/SKILL.md'
|
|
638
|
+
)
|
|
639
|
+
await writeDocument(
|
|
640
|
+
cachedSkillPath,
|
|
641
|
+
'---\nname: frontend-design\ndescription: Cached UI guidance\n---\nUse the cached copy.\n'
|
|
642
|
+
)
|
|
643
|
+
await writeDocument(
|
|
644
|
+
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
645
|
+
[
|
|
646
|
+
'---',
|
|
647
|
+
'name: app-builder',
|
|
648
|
+
'description: Build apps',
|
|
649
|
+
'dependencies:',
|
|
650
|
+
' - anthropics/skills@frontend-design',
|
|
651
|
+
'---',
|
|
652
|
+
'Build the app.'
|
|
653
|
+
].join('\n')
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
657
|
+
cwd: workspace,
|
|
658
|
+
configs: [undefined, undefined],
|
|
659
|
+
useDefaultVibeForgeMcpServer: false
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
await buildAdapterAssetPlan({
|
|
663
|
+
adapter: 'opencode',
|
|
664
|
+
bundle,
|
|
665
|
+
options: {
|
|
666
|
+
skills: {
|
|
667
|
+
include: ['app-builder']
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
|
|
673
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
674
|
+
expect(await readFile(cachedSkillPath, 'utf8')).toContain('Use the cached copy.')
|
|
675
|
+
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('parses multi-segment source dependencies and forwards skillsCli runtime config', async () => {
|
|
679
|
+
const workspace = await createWorkspace()
|
|
680
|
+
const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
|
|
681
|
+
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
|
|
682
|
+
await writeDocument(
|
|
683
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
684
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design tokens.\n'
|
|
685
|
+
)
|
|
686
|
+
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
687
|
+
tempDir: tempInstallDir,
|
|
688
|
+
installedSkill: {
|
|
689
|
+
dirName: 'frontend-design',
|
|
690
|
+
name: 'frontend-design',
|
|
691
|
+
sourcePath: installedSkillDir
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
await writeDocument(
|
|
696
|
+
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
697
|
+
[
|
|
698
|
+
'---',
|
|
699
|
+
'name: app-builder',
|
|
700
|
+
'description: Build apps',
|
|
701
|
+
'dependencies:',
|
|
702
|
+
' - example-source/default/public/frontend-design',
|
|
703
|
+
'---',
|
|
704
|
+
'Build the app.'
|
|
705
|
+
].join('\n')
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
709
|
+
cwd: workspace,
|
|
710
|
+
configs: [{
|
|
711
|
+
skillsCli: {
|
|
712
|
+
registry: 'https://registry.example.com'
|
|
713
|
+
}
|
|
714
|
+
}, undefined],
|
|
715
|
+
useDefaultVibeForgeMcpServer: false
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
await buildAdapterAssetPlan({
|
|
719
|
+
adapter: 'opencode',
|
|
720
|
+
bundle,
|
|
721
|
+
options: {
|
|
722
|
+
skills: {
|
|
723
|
+
include: ['app-builder']
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
729
|
+
config: {
|
|
730
|
+
registry: 'https://registry.example.com'
|
|
731
|
+
},
|
|
732
|
+
skill: 'frontend-design',
|
|
733
|
+
source: 'example-source/default/public'
|
|
734
|
+
})
|
|
735
|
+
})
|
|
736
|
+
|
|
100
737
|
it('loads workspace entities from the env-configured entities dir', async () => {
|
|
101
738
|
const workspace = await createWorkspace()
|
|
102
739
|
const previousEntitiesDir = process.env.__VF_PROJECT_AI_ENTITIES_DIR__
|
|
@@ -201,6 +838,44 @@ describe('resolveWorkspaceAssetBundle', () => {
|
|
|
201
838
|
expect(disabledBundle.mcpServers).not.toHaveProperty('VibeForge')
|
|
202
839
|
})
|
|
203
840
|
|
|
841
|
+
it('discovers configured workspaces from glob patterns and entries', async () => {
|
|
842
|
+
const workspace = await createWorkspace()
|
|
843
|
+
|
|
844
|
+
await writeDocument(join(workspace, 'services/billing/README.md'), '# billing\n')
|
|
845
|
+
await writeDocument(join(workspace, 'services/legacy/README.md'), '# legacy\n')
|
|
846
|
+
await writeDocument(join(workspace, 'docs/README.md'), '# docs\n')
|
|
847
|
+
|
|
848
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
849
|
+
cwd: workspace,
|
|
850
|
+
configs: [{
|
|
851
|
+
workspaces: {
|
|
852
|
+
include: ['services/*'],
|
|
853
|
+
exclude: ['services/legacy'],
|
|
854
|
+
entries: {
|
|
855
|
+
docs: {
|
|
856
|
+
path: 'docs',
|
|
857
|
+
description: 'Documentation workspace'
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}, undefined],
|
|
862
|
+
useDefaultVibeForgeMcpServer: false
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
expect(bundle.workspaces.map(asset => asset.displayName)).toEqual(['billing', 'docs'])
|
|
866
|
+
expect(bundle.workspaces.map(asset => asset.payload)).toEqual([
|
|
867
|
+
expect.objectContaining({
|
|
868
|
+
id: 'billing',
|
|
869
|
+
path: 'services/billing'
|
|
870
|
+
}),
|
|
871
|
+
expect.objectContaining({
|
|
872
|
+
id: 'docs',
|
|
873
|
+
path: 'docs',
|
|
874
|
+
description: 'Documentation workspace'
|
|
875
|
+
})
|
|
876
|
+
])
|
|
877
|
+
})
|
|
878
|
+
|
|
204
879
|
it('skips disabled plugin instances and lets disabled child overrides suppress default child activation', async () => {
|
|
205
880
|
const workspace = await createWorkspace()
|
|
206
881
|
|