@vibe-forge/workspace-assets 3.2.1 → 3.2.2-alpha.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.
- package/__tests__/adapter-asset-plan.spec.ts +0 -58
- package/__tests__/bundle.spec.ts +0 -238
- package/__tests__/skill-dependencies-cli.spec.ts +59 -252
- package/package.json +4 -4
- package/src/bundle-internal.ts +48 -1
- package/src/plugin-skill-dependencies.ts +1 -0
- package/src/selection-internal.ts +9 -2
- package/src/skill-dependencies.ts +10 -108
- package/src/skills-cli-dependency-helpers.ts +0 -94
- package/src/skills-cli-dependency.ts +0 -133
|
@@ -280,64 +280,6 @@ describe('buildAdapterAssetPlan', () => {
|
|
|
280
280
|
]))
|
|
281
281
|
})
|
|
282
282
|
|
|
283
|
-
it('keeps explicit skills CLI dependencies ahead of preselected home-bridged skills in overlays', async () => {
|
|
284
|
-
const workspace = await createWorkspace()
|
|
285
|
-
const realHome = process.env.__VF_PROJECT_REAL_HOME__
|
|
286
|
-
const tempInstallDir = join(workspace, '.tmp-install-skills-cli-foo')
|
|
287
|
-
const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'foo')
|
|
288
|
-
await writeDocument(
|
|
289
|
-
join(installedSkillDir, 'SKILL.md'),
|
|
290
|
-
'---\nname: foo\ndescription: Registry foo\n---\nUse the registry definition.\n'
|
|
291
|
-
)
|
|
292
|
-
skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
293
|
-
tempDir: tempInstallDir,
|
|
294
|
-
installedSkill: {
|
|
295
|
-
dirName: 'foo',
|
|
296
|
-
name: 'foo',
|
|
297
|
-
sourcePath: installedSkillDir
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
await writeDocument(
|
|
302
|
-
join(realHome!, '.agents/skills/foo/SKILL.md'),
|
|
303
|
-
'---\ndescription: Home foo\n---\nUse the home definition.\n'
|
|
304
|
-
)
|
|
305
|
-
await writeDocument(
|
|
306
|
-
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
307
|
-
[
|
|
308
|
-
'---',
|
|
309
|
-
'name: app-builder',
|
|
310
|
-
'description: Build apps',
|
|
311
|
-
'dependencies:',
|
|
312
|
-
' - anthropics/skills@foo',
|
|
313
|
-
'---',
|
|
314
|
-
'Build the app.'
|
|
315
|
-
].join('\n')
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
const bundle = await resolveWorkspaceAssetBundle({
|
|
319
|
-
cwd: workspace,
|
|
320
|
-
configs: [undefined, undefined],
|
|
321
|
-
useDefaultVibeForgeMcpServer: false
|
|
322
|
-
})
|
|
323
|
-
const plan = await buildAdapterAssetPlan({
|
|
324
|
-
adapter: 'opencode',
|
|
325
|
-
bundle,
|
|
326
|
-
options: {
|
|
327
|
-
skills: {
|
|
328
|
-
include: ['foo', 'app-builder']
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
const fooOverlays = plan.overlays.filter(entry => entry.kind === 'skill' && entry.targetPath === 'skills/foo')
|
|
334
|
-
expect(fooOverlays).toHaveLength(1)
|
|
335
|
-
expect(fooOverlays[0]?.sourcePath).toContain(
|
|
336
|
-
'/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/foo'
|
|
337
|
-
)
|
|
338
|
-
expect(fooOverlays[0]?.sourcePath).not.toBe(join(realHome!, '.agents/skills/foo'))
|
|
339
|
-
})
|
|
340
|
-
|
|
341
283
|
it('prunes excluded skill dependency subtrees from selected native overlays', async () => {
|
|
342
284
|
const workspace = await createWorkspace()
|
|
343
285
|
|
package/__tests__/bundle.spec.ts
CHANGED
|
@@ -386,79 +386,6 @@ describe('resolveWorkspaceAssetBundle', () => {
|
|
|
386
386
|
}))
|
|
387
387
|
})
|
|
388
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/latest/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
389
|
it('installs configured project skills before bundle resolution and rewrites renamed skill names', async () => {
|
|
463
390
|
const workspace = await createWorkspace()
|
|
464
391
|
const tempInstallDir = join(workspace, '.tmp-configured-install')
|
|
@@ -564,171 +491,6 @@ describe('resolveWorkspaceAssetBundle', () => {
|
|
|
564
491
|
)
|
|
565
492
|
})
|
|
566
493
|
|
|
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/latest/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/latest/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 registry/source/version dependency specs and forwards them to the skills CLI installer', 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
|
-
' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
|
|
703
|
-
'---',
|
|
704
|
-
'Build the app.'
|
|
705
|
-
].join('\n')
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
const bundle = await resolveWorkspaceAssetBundle({
|
|
709
|
-
cwd: workspace,
|
|
710
|
-
configs: [undefined, undefined],
|
|
711
|
-
useDefaultVibeForgeMcpServer: false
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
await buildAdapterAssetPlan({
|
|
715
|
-
adapter: 'opencode',
|
|
716
|
-
bundle,
|
|
717
|
-
options: {
|
|
718
|
-
skills: {
|
|
719
|
-
include: ['app-builder']
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
725
|
-
registry: 'https://registry.example.com',
|
|
726
|
-
skill: 'frontend-design',
|
|
727
|
-
source: 'example-source/default/public',
|
|
728
|
-
version: '1.0.3'
|
|
729
|
-
})
|
|
730
|
-
})
|
|
731
|
-
|
|
732
494
|
it('loads workspace entities from the env-configured entities dir', async () => {
|
|
733
495
|
const workspace = await createWorkspace()
|
|
734
496
|
const previousEntitiesDir = process.env.__VF_PROJECT_AI_ENTITIES_DIR__
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/* eslint-disable import/first -- hoisted vitest mocks must be declared before importing the bundle entrypoint */
|
|
2
|
-
import {
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path, { join } from 'node:path'
|
|
2
|
+
import { join } from 'node:path'
|
|
5
3
|
|
|
6
|
-
import {
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
5
|
|
|
8
6
|
const mocks = vi.hoisted(() => ({
|
|
9
7
|
findSkillsCli: vi.fn(),
|
|
@@ -23,54 +21,15 @@ vi.mock('@vibe-forge/utils/skills-cli', async () => {
|
|
|
23
21
|
|
|
24
22
|
import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
|
|
25
23
|
|
|
26
|
-
import { createWorkspace, writeDocument } from './test-helpers'
|
|
24
|
+
import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
|
|
27
25
|
|
|
28
|
-
describe('
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const pathExists = async (targetPath: string) => {
|
|
32
|
-
try {
|
|
33
|
-
await access(targetPath)
|
|
34
|
-
return true
|
|
35
|
-
} catch {
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
beforeEach(async () => {
|
|
41
|
-
installWorkspace = await mkdtemp(path.join(os.tmpdir(), 'vf-skills-cli-dependency-'))
|
|
26
|
+
describe('materialized skill dependency resolution', () => {
|
|
27
|
+
beforeEach(() => {
|
|
42
28
|
vi.clearAllMocks()
|
|
43
29
|
})
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
await rm(installWorkspace, { recursive: true, force: true })
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('installs missing bare-name dependencies through skills CLI by default', async () => {
|
|
31
|
+
it('fails missing dependencies without downloading during runtime preparation', async () => {
|
|
50
32
|
const workspace = await createWorkspace()
|
|
51
|
-
const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
|
|
52
|
-
await mkdir(installedSkillDir, { recursive: true })
|
|
53
|
-
await writeFile(
|
|
54
|
-
join(installedSkillDir, 'SKILL.md'),
|
|
55
|
-
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
mocks.findSkillsCli.mockResolvedValue([
|
|
59
|
-
{
|
|
60
|
-
installRef: 'anthropics/skills@frontend-design',
|
|
61
|
-
source: 'anthropics/skills',
|
|
62
|
-
skill: 'frontend-design'
|
|
63
|
-
}
|
|
64
|
-
])
|
|
65
|
-
mocks.installSkillsCliRefToTemp.mockResolvedValue({
|
|
66
|
-
tempDir: installWorkspace,
|
|
67
|
-
installedSkill: {
|
|
68
|
-
dirName: 'frontend-design',
|
|
69
|
-
name: 'frontend-design',
|
|
70
|
-
sourcePath: installedSkillDir
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
|
|
74
33
|
await writeDocument(
|
|
75
34
|
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
76
35
|
[
|
|
@@ -90,57 +49,6 @@ describe('skills CLI dependency resolution', () => {
|
|
|
90
49
|
useDefaultVibeForgeMcpServer: false
|
|
91
50
|
})
|
|
92
51
|
|
|
93
|
-
await buildAdapterAssetPlan({
|
|
94
|
-
adapter: 'opencode',
|
|
95
|
-
bundle,
|
|
96
|
-
options: {
|
|
97
|
-
skills: {
|
|
98
|
-
include: ['app-builder']
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
|
|
104
|
-
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
105
|
-
expect(dependency?.sourcePath).toContain(
|
|
106
|
-
'/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/'
|
|
107
|
-
)
|
|
108
|
-
expect(mocks.findSkillsCli).toHaveBeenCalledWith({
|
|
109
|
-
query: 'frontend-design'
|
|
110
|
-
})
|
|
111
|
-
expect(mocks.installSkillsCliRefToTemp).toHaveBeenCalledWith({
|
|
112
|
-
installRef: 'anthropics/skills@frontend-design'
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('blocks missing dependency installs when auto downloads are disabled', async () => {
|
|
117
|
-
const workspace = await createWorkspace()
|
|
118
|
-
|
|
119
|
-
await writeDocument(
|
|
120
|
-
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
121
|
-
[
|
|
122
|
-
'---',
|
|
123
|
-
'name: app-builder',
|
|
124
|
-
'description: Build apps',
|
|
125
|
-
'dependencies:',
|
|
126
|
-
' - name: frontend-design',
|
|
127
|
-
' source: anthropics/skills',
|
|
128
|
-
' registry: https://dependency-registry.example.test',
|
|
129
|
-
'---',
|
|
130
|
-
'Build the app.'
|
|
131
|
-
].join('\n')
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const bundle = await resolveWorkspaceAssetBundle({
|
|
135
|
-
cwd: workspace,
|
|
136
|
-
configs: [{
|
|
137
|
-
skills: {
|
|
138
|
-
autoDownloadDependencies: false
|
|
139
|
-
}
|
|
140
|
-
}, undefined],
|
|
141
|
-
useDefaultVibeForgeMcpServer: false
|
|
142
|
-
})
|
|
143
|
-
|
|
144
52
|
await expect(buildAdapterAssetPlan({
|
|
145
53
|
adapter: 'opencode',
|
|
146
54
|
bundle,
|
|
@@ -149,17 +57,15 @@ describe('skills CLI dependency resolution', () => {
|
|
|
149
57
|
include: ['app-builder']
|
|
150
58
|
}
|
|
151
59
|
}
|
|
152
|
-
})).rejects.toThrow('
|
|
60
|
+
})).rejects.toThrow('Run vf skills install or vf skills update')
|
|
153
61
|
|
|
154
62
|
expect(mocks.findSkillsCli).not.toHaveBeenCalled()
|
|
155
63
|
expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
|
|
156
64
|
expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
157
|
-
expect(bundle.skills.map(asset => asset.name)).toEqual(['app-builder'])
|
|
158
65
|
})
|
|
159
66
|
|
|
160
|
-
it('
|
|
67
|
+
it('uses project-materialized dependencies from .ai/skills', async () => {
|
|
161
68
|
const workspace = await createWorkspace()
|
|
162
|
-
|
|
163
69
|
await writeDocument(
|
|
164
70
|
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
165
71
|
[
|
|
@@ -172,67 +78,17 @@ describe('skills CLI dependency resolution', () => {
|
|
|
172
78
|
'Build the app.'
|
|
173
79
|
].join('\n')
|
|
174
80
|
)
|
|
175
|
-
|
|
176
|
-
const bundle = await resolveWorkspaceAssetBundle({
|
|
177
|
-
cwd: workspace,
|
|
178
|
-
configs: [{
|
|
179
|
-
skills: {
|
|
180
|
-
autoDownloadDependencies: false
|
|
181
|
-
}
|
|
182
|
-
}, undefined],
|
|
183
|
-
useDefaultVibeForgeMcpServer: false
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
await expect(buildAdapterAssetPlan({
|
|
187
|
-
adapter: 'opencode',
|
|
188
|
-
bundle,
|
|
189
|
-
options: {
|
|
190
|
-
skills: {
|
|
191
|
-
include: ['app-builder']
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
})).rejects.toThrow(
|
|
195
|
-
'Skill dependency automatic downloads are disabled; cannot resolve frontend-design without a source'
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
expect(mocks.findSkillsCli).not.toHaveBeenCalled()
|
|
199
|
-
expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
|
|
200
|
-
expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('reuses source-qualified dependency caches when auto downloads are disabled', async () => {
|
|
204
|
-
const workspace = await createWorkspace()
|
|
205
|
-
await writeDocument(
|
|
206
|
-
join(
|
|
207
|
-
workspace,
|
|
208
|
-
'.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/SKILL.md'
|
|
209
|
-
),
|
|
210
|
-
'---\nname: frontend-design\ndescription: Cached UI guidance\n---\nUse the cached dependency.\n'
|
|
211
|
-
)
|
|
212
81
|
await writeDocument(
|
|
213
|
-
join(workspace, '.ai/skills/
|
|
214
|
-
|
|
215
|
-
'---',
|
|
216
|
-
'name: app-builder',
|
|
217
|
-
'description: Build apps',
|
|
218
|
-
'dependencies:',
|
|
219
|
-
' - anthropics/skills@frontend-design',
|
|
220
|
-
'---',
|
|
221
|
-
'Build the app.'
|
|
222
|
-
].join('\n')
|
|
82
|
+
join(workspace, '.ai/skills/frontend-design/SKILL.md'),
|
|
83
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
|
|
223
84
|
)
|
|
224
85
|
|
|
225
86
|
const bundle = await resolveWorkspaceAssetBundle({
|
|
226
87
|
cwd: workspace,
|
|
227
|
-
configs: [
|
|
228
|
-
skills: {
|
|
229
|
-
autoDownloadDependencies: false
|
|
230
|
-
}
|
|
231
|
-
}, undefined],
|
|
88
|
+
configs: [undefined, undefined],
|
|
232
89
|
useDefaultVibeForgeMcpServer: false
|
|
233
90
|
})
|
|
234
|
-
|
|
235
|
-
await buildAdapterAssetPlan({
|
|
91
|
+
const plan = await buildAdapterAssetPlan({
|
|
236
92
|
adapter: 'opencode',
|
|
237
93
|
bundle,
|
|
238
94
|
options: {
|
|
@@ -242,128 +98,79 @@ describe('skills CLI dependency resolution', () => {
|
|
|
242
98
|
}
|
|
243
99
|
})
|
|
244
100
|
|
|
101
|
+
expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
|
|
102
|
+
'skills/app-builder',
|
|
103
|
+
'skills/frontend-design'
|
|
104
|
+
])
|
|
245
105
|
expect(mocks.findSkillsCli).not.toHaveBeenCalled()
|
|
246
|
-
expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
|
|
247
|
-
expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
248
|
-
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
249
106
|
})
|
|
250
107
|
|
|
251
|
-
it('
|
|
108
|
+
it('loads plugin dependencies from .ai/skills/.plugins through the lockfile', async () => {
|
|
252
109
|
const workspace = await createWorkspace()
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
join(installedSkillDir, 'SKILL.md'),
|
|
257
|
-
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design system.\n'
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
mocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
261
|
-
tempDir: installWorkspace,
|
|
262
|
-
installedSkill: {
|
|
263
|
-
dirName: 'frontend-design',
|
|
264
|
-
name: 'frontend-design',
|
|
265
|
-
sourcePath: installedSkillDir
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
await writeDocument(
|
|
270
|
-
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
271
|
-
[
|
|
110
|
+
await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
|
|
111
|
+
'package.json': JSON.stringify({ name: '@vibe-forge/plugin-review', version: '1.0.0' }, null, 2),
|
|
112
|
+
'skills/review-helper/SKILL.md': [
|
|
272
113
|
'---',
|
|
273
|
-
'name:
|
|
274
|
-
'description:
|
|
114
|
+
'name: review-helper',
|
|
115
|
+
'description: Review helper',
|
|
275
116
|
'dependencies:',
|
|
276
|
-
' -
|
|
117
|
+
' - shared-runtime',
|
|
277
118
|
'---',
|
|
278
|
-
'
|
|
119
|
+
'Review code.'
|
|
279
120
|
].join('\n')
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
const bundle = await resolveWorkspaceAssetBundle({
|
|
283
|
-
cwd: workspace,
|
|
284
|
-
configs: [undefined, undefined],
|
|
285
|
-
useDefaultVibeForgeMcpServer: false
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
await buildAdapterAssetPlan({
|
|
289
|
-
adapter: 'opencode',
|
|
290
|
-
bundle,
|
|
291
|
-
options: {
|
|
292
|
-
skills: {
|
|
293
|
-
include: ['app-builder']
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
121
|
})
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
skill: 'frontend-design',
|
|
301
|
-
source: 'example-source/default/public',
|
|
302
|
-
version: '1.0.3'
|
|
303
|
-
})
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
it('clears stale dependency install locks before retrying a direct startup install', async () => {
|
|
307
|
-
const workspace = await createWorkspace()
|
|
308
|
-
const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'lynx-cat')
|
|
309
|
-
await mkdir(installedSkillDir, { recursive: true })
|
|
310
|
-
await writeFile(
|
|
311
|
-
join(installedSkillDir, 'SKILL.md'),
|
|
312
|
-
'---\nname: lynx-cat\ndescription: Lynx helper\n---\nDebug Lynx apps.\n'
|
|
122
|
+
await writeDocument(
|
|
123
|
+
join(workspace, '.ai/skills/.plugins/review/shared-runtime/SKILL.md'),
|
|
124
|
+
'---\nname: shared-runtime\ndescription: Shared runtime\n---\nShared plugin dependency.\n'
|
|
313
125
|
)
|
|
314
|
-
|
|
315
|
-
mocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
316
|
-
tempDir: installWorkspace,
|
|
317
|
-
installedSkill: {
|
|
318
|
-
dirName: 'lynx-cat',
|
|
319
|
-
name: 'lynx-cat',
|
|
320
|
-
sourcePath: installedSkillDir
|
|
321
|
-
}
|
|
322
|
-
})
|
|
323
|
-
|
|
324
126
|
await writeDocument(
|
|
325
|
-
join(workspace, '.ai/skills
|
|
127
|
+
join(workspace, '.ai/skills.lock.yaml'),
|
|
326
128
|
[
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'
|
|
129
|
+
'version: 1',
|
|
130
|
+
'pluginSkills:',
|
|
131
|
+
' review/shared-runtime:',
|
|
132
|
+
' name: shared-runtime',
|
|
133
|
+
' requested: false',
|
|
134
|
+
' pluginInstance: review',
|
|
135
|
+
' pluginInstancePath: "0"',
|
|
136
|
+
' installPath: .ai/skills/.plugins/review/shared-runtime',
|
|
137
|
+
' dependencyOf:',
|
|
138
|
+
' - plugin:review/review-helper',
|
|
139
|
+
' source: vendor/shared-skills',
|
|
140
|
+
' version: 1.0.0',
|
|
141
|
+
' hash: sha256:test',
|
|
142
|
+
' installedAt: "2026-05-13T00:00:00.000Z"'
|
|
334
143
|
].join('\n')
|
|
335
144
|
)
|
|
336
145
|
|
|
337
|
-
const lockDir = join(
|
|
338
|
-
workspace,
|
|
339
|
-
'.ai/caches/skill-dependencies/skills-cli/skills/latest/https-registry.example.com/example-source/lynx/skills/latest/lynx-cat.lock'
|
|
340
|
-
)
|
|
341
|
-
await mkdir(lockDir, { recursive: true })
|
|
342
|
-
await utimes(lockDir, new Date(Date.now() - 120_000), new Date(Date.now() - 120_000))
|
|
343
|
-
|
|
344
146
|
const bundle = await resolveWorkspaceAssetBundle({
|
|
345
147
|
cwd: workspace,
|
|
346
|
-
configs: [
|
|
148
|
+
configs: [{
|
|
149
|
+
plugins: [{
|
|
150
|
+
id: '@vibe-forge/plugin-review',
|
|
151
|
+
scope: 'review'
|
|
152
|
+
}]
|
|
153
|
+
}, undefined],
|
|
347
154
|
useDefaultVibeForgeMcpServer: false
|
|
348
155
|
})
|
|
349
|
-
|
|
350
|
-
await buildAdapterAssetPlan({
|
|
156
|
+
const plan = await buildAdapterAssetPlan({
|
|
351
157
|
adapter: 'opencode',
|
|
352
158
|
bundle,
|
|
353
159
|
options: {
|
|
354
160
|
skills: {
|
|
355
|
-
include: ['
|
|
161
|
+
include: ['review/review-helper']
|
|
356
162
|
}
|
|
357
163
|
}
|
|
358
164
|
})
|
|
359
165
|
|
|
360
|
-
expect(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
166
|
+
expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual([
|
|
167
|
+
'review/review-helper',
|
|
168
|
+
'review/shared-runtime'
|
|
169
|
+
])
|
|
170
|
+
expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
|
|
171
|
+
'skills/review__review-helper',
|
|
172
|
+
'skills/review__shared-runtime'
|
|
173
|
+
])
|
|
174
|
+
expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
|
|
368
175
|
})
|
|
369
176
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-forge/workspace-assets",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2-alpha.0",
|
|
4
4
|
"description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
|
|
5
5
|
"imports": {
|
|
6
6
|
"#~/*.js": {
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"fast-glob": "^3.3.3",
|
|
30
30
|
"front-matter": "^4.0.2",
|
|
31
31
|
"js-yaml": "^4.1.1",
|
|
32
|
+
"@vibe-forge/config": "3.2.3-alpha.0",
|
|
33
|
+
"@vibe-forge/types": "3.2.3-alpha.0",
|
|
32
34
|
"@vibe-forge/definition-core": "3.2.0",
|
|
33
|
-
"@vibe-forge/
|
|
34
|
-
"@vibe-forge/types": "3.2.2",
|
|
35
|
-
"@vibe-forge/utils": "3.2.2"
|
|
35
|
+
"@vibe-forge/utils": "3.2.3-alpha.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/js-yaml": "^4.0.9"
|
package/src/bundle-internal.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { access, readFile } from 'node:fs/promises'
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, resolve } from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
|
|
12
12
|
import {
|
|
13
13
|
isObjectSkillsConfig,
|
|
14
|
+
readProjectSkillsLockfile,
|
|
14
15
|
resolveProjectAiBaseDir,
|
|
15
16
|
resolveProjectAiEntitiesDir,
|
|
16
17
|
resolveRelativePath
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
} from '@vibe-forge/definition-core'
|
|
36
37
|
import { ensureConfiguredProjectSkills } from './configured-skills'
|
|
37
38
|
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
39
|
+
import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
|
|
38
40
|
import { resolveConfiguredWorkspaceAssets } from './workspaces'
|
|
39
41
|
|
|
40
42
|
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
@@ -440,6 +442,40 @@ const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
|
|
|
440
442
|
}
|
|
441
443
|
}
|
|
442
444
|
|
|
445
|
+
const pathExists = async (path: string) => {
|
|
446
|
+
try {
|
|
447
|
+
await access(path)
|
|
448
|
+
return true
|
|
449
|
+
} catch {
|
|
450
|
+
return false
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const scanPluginDependencySkillDocuments = async (
|
|
455
|
+
cwd: string,
|
|
456
|
+
instances: ResolvedPluginInstance[]
|
|
457
|
+
) => {
|
|
458
|
+
const lockfile = await readProjectSkillsLockfile(cwd)
|
|
459
|
+
const documents: Array<{ instance: ResolvedPluginInstance; path: string }> = []
|
|
460
|
+
for (const entry of Object.values(lockfile.pluginSkills ?? {})) {
|
|
461
|
+
const instance = instances.find(candidate => (
|
|
462
|
+
candidate.instancePath === entry.pluginInstancePath ||
|
|
463
|
+
candidate.scope === entry.pluginInstance
|
|
464
|
+
))
|
|
465
|
+
if (instance == null) continue
|
|
466
|
+
|
|
467
|
+
const skillPath = resolve(cwd, entry.installPath, 'SKILL.md')
|
|
468
|
+
if (await pathExists(skillPath)) {
|
|
469
|
+
documents.push({
|
|
470
|
+
instance,
|
|
471
|
+
path: skillPath
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return documents
|
|
477
|
+
}
|
|
478
|
+
|
|
443
479
|
const toOpenCodeOverlayEntries = (
|
|
444
480
|
kind: OpenCodeOverlayKind,
|
|
445
481
|
targetDir: 'agents' | 'commands' | 'modes' | 'plugins',
|
|
@@ -578,6 +614,7 @@ export async function collectWorkspaceAssets(params: {
|
|
|
578
614
|
])
|
|
579
615
|
const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
|
|
580
616
|
const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
|
|
617
|
+
const pluginDependencySkillDocs = await scanPluginDependencySkillDocuments(params.cwd, flattenedPluginInstances)
|
|
581
618
|
const pluginOverlayScans = await Promise.all(
|
|
582
619
|
flattenedPluginInstances.map(instance => scanInstanceOpenCodeOverlays(instance))
|
|
583
620
|
)
|
|
@@ -631,6 +668,16 @@ export async function collectWorkspaceAssets(params: {
|
|
|
631
668
|
await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
|
|
632
669
|
await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
|
|
633
670
|
}
|
|
671
|
+
for (const entry of pluginDependencySkillDocs) {
|
|
672
|
+
await pushDocumentAssets(
|
|
673
|
+
'skill',
|
|
674
|
+
[entry.path],
|
|
675
|
+
'plugin',
|
|
676
|
+
entry.instance,
|
|
677
|
+
undefined,
|
|
678
|
+
PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
|
|
679
|
+
)
|
|
680
|
+
}
|
|
634
681
|
await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
|
|
635
682
|
|
|
636
683
|
const skills = mergeSkillAssets(skillAssets)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY = 'plugin-skill-dependency-lock'
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isRemoteRuleReference,
|
|
27
27
|
parseScopedReference
|
|
28
28
|
} from '@vibe-forge/definition-core'
|
|
29
|
+
import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
|
|
29
30
|
import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
|
|
30
31
|
|
|
31
32
|
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
@@ -359,6 +360,10 @@ const mergeEntityDefinitions = (
|
|
|
359
360
|
|
|
360
361
|
const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
|
|
361
362
|
|
|
363
|
+
const isPluginSkillDependencyAsset = (asset: Extract<WorkspaceAsset, { kind: 'skill' }>) => (
|
|
364
|
+
asset.resolvedBy === PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
|
|
365
|
+
)
|
|
366
|
+
|
|
362
367
|
const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
|
|
363
368
|
[...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
|
|
364
369
|
.map(item => item.displayName)
|
|
@@ -517,9 +522,10 @@ export const resolveSelectedSkillAssets = (
|
|
|
517
522
|
): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
|
|
518
523
|
if (selection == null) return assets
|
|
519
524
|
|
|
525
|
+
const rootAssets = assets.filter(asset => !isPluginSkillDependencyAsset(asset))
|
|
520
526
|
const included = selection.include != null && selection.include.length > 0
|
|
521
527
|
? resolveNamedAssets(assets, selection.include)
|
|
522
|
-
:
|
|
528
|
+
: rootAssets
|
|
523
529
|
const excluded = new Set(
|
|
524
530
|
resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
|
|
525
531
|
)
|
|
@@ -532,9 +538,10 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
|
|
|
532
538
|
bundle: WorkspaceAssetBundle,
|
|
533
539
|
selection?: WorkspaceSkillSelection
|
|
534
540
|
): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
|
|
541
|
+
const rootAssets = bundle.skills.filter(asset => !isPluginSkillDependencyAsset(asset))
|
|
535
542
|
const included = selection?.include != null && selection.include.length > 0
|
|
536
543
|
? resolveNamedAssets(bundle.skills, selection.include)
|
|
537
|
-
:
|
|
544
|
+
: rootAssets
|
|
538
545
|
const excluded = new Set(
|
|
539
546
|
resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
|
|
540
547
|
)
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
/* eslint-disable max-lines -- dependency normalization and graph expansion share the same local helpers */
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import { parseScopedReference, resolveSkillIdentifier } from '@vibe-forge/definition-core'
|
|
7
|
-
import type { Config, Definition, Skill, WorkspaceAsset } from '@vibe-forge/types'
|
|
8
|
-
import { formatSkillsSpec, parseSkillsSpec, resolveRelativePath } from '@vibe-forge/utils'
|
|
2
|
+
import { parseScopedReference } from '@vibe-forge/definition-core'
|
|
3
|
+
import type { Config, Skill, WorkspaceAsset } from '@vibe-forge/types'
|
|
4
|
+
import { formatSkillsSpec, parseSkillsSpec } from '@vibe-forge/utils'
|
|
9
5
|
|
|
10
6
|
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
11
|
-
import { installSkillsCliDependency } from './skills-cli-dependency'
|
|
12
7
|
|
|
13
8
|
type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
|
|
14
9
|
|
|
@@ -106,39 +101,6 @@ const findSkillAssetByRef = (
|
|
|
106
101
|
return resolveUniqueSkillByName(searchableAssets, ref)
|
|
107
102
|
}
|
|
108
103
|
|
|
109
|
-
const resolveDisplayName = (name: string, scope?: string) => (
|
|
110
|
-
scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const parseFrontmatterSkill = async (path: string): Promise<Definition<Skill>> => {
|
|
114
|
-
const content = await readFile(path, 'utf-8')
|
|
115
|
-
const { body, attributes } = fm<Skill>(content)
|
|
116
|
-
return {
|
|
117
|
-
path,
|
|
118
|
-
body,
|
|
119
|
-
attributes
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const createResolvedSkillAsset = (params: {
|
|
124
|
-
cwd: string
|
|
125
|
-
definition: Definition<Skill>
|
|
126
|
-
}) => {
|
|
127
|
-
const name = resolveSkillIdentifier(params.definition.path, params.definition.attributes.name)
|
|
128
|
-
const displayName = resolveDisplayName(name)
|
|
129
|
-
return {
|
|
130
|
-
id: `skill:workspace:workspace:${displayName}:${resolveRelativePath(params.cwd, params.definition.path)}`,
|
|
131
|
-
kind: 'skill',
|
|
132
|
-
name,
|
|
133
|
-
displayName,
|
|
134
|
-
origin: 'workspace',
|
|
135
|
-
sourcePath: params.definition.path,
|
|
136
|
-
payload: {
|
|
137
|
-
definition: params.definition
|
|
138
|
-
}
|
|
139
|
-
} satisfies SkillAsset
|
|
140
|
-
}
|
|
141
|
-
|
|
142
104
|
export const normalizeSkillDependency = (value: unknown): NormalizedSkillDependency | undefined => {
|
|
143
105
|
const stringValue = asNonEmptyString(value)
|
|
144
106
|
if (stringValue != null) return parseSkillsSpec(stringValue)
|
|
@@ -230,8 +192,6 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
|
|
|
230
192
|
) => {
|
|
231
193
|
const selected: SkillAsset[] = []
|
|
232
194
|
const seen = new Set<string>()
|
|
233
|
-
const fetchedDependencyRefs = new Set<string>()
|
|
234
|
-
|
|
235
195
|
const removeSupersededHomeBridgeSkill = (displayName: string) => {
|
|
236
196
|
removeHomeBridgeSkillDuplicates(params.allAssets, displayName)
|
|
237
197
|
removeHomeBridgeSkillDuplicates(params.skillAssets, displayName)
|
|
@@ -239,54 +199,6 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
|
|
|
239
199
|
removeHomeBridgeSkillDuplicates(selected, displayName)
|
|
240
200
|
}
|
|
241
201
|
|
|
242
|
-
const installDependencyAsset = async (
|
|
243
|
-
dependency: NormalizedSkillDependency,
|
|
244
|
-
currentInstancePath?: string
|
|
245
|
-
) => {
|
|
246
|
-
const fetchKey = dependency.ref
|
|
247
|
-
if (!fetchedDependencyRefs.has(fetchKey)) {
|
|
248
|
-
fetchedDependencyRefs.add(fetchKey)
|
|
249
|
-
const installed = await installSkillsCliDependency({
|
|
250
|
-
cwd: params.cwd,
|
|
251
|
-
configs: params.configs,
|
|
252
|
-
dependency
|
|
253
|
-
})
|
|
254
|
-
const definition = await parseFrontmatterSkill(installed.skillPath)
|
|
255
|
-
const dependencyAsset = createResolvedSkillAsset({
|
|
256
|
-
cwd: params.cwd,
|
|
257
|
-
definition
|
|
258
|
-
})
|
|
259
|
-
const existingAsset = findSkillDependencyAsset(
|
|
260
|
-
params.skillAssets,
|
|
261
|
-
dependency,
|
|
262
|
-
currentInstancePath,
|
|
263
|
-
{ includeHomeBridge: false }
|
|
264
|
-
) ??
|
|
265
|
-
params.skillAssets.find(existing => (
|
|
266
|
-
existing.resolvedBy !== HOME_BRIDGE_RESOLVED_BY &&
|
|
267
|
-
existing.displayName === dependencyAsset.displayName
|
|
268
|
-
))
|
|
269
|
-
if (existingAsset != null) {
|
|
270
|
-
removeSupersededHomeBridgeSkill(existingAsset.displayName)
|
|
271
|
-
return existingAsset
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
removeSupersededHomeBridgeSkill(dependencyAsset.displayName)
|
|
275
|
-
params.allAssets.push(dependencyAsset)
|
|
276
|
-
params.skillAssets.push(dependencyAsset)
|
|
277
|
-
return dependencyAsset
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// After the first fetch attempt, reuse whichever asset is now visible in the
|
|
281
|
-
// skill set: a newly installed registry skill, or a home-bridge fallback
|
|
282
|
-
// that was accepted by an earlier plain-name dependency resolution.
|
|
283
|
-
const resolvedAsset = findSkillDependencyAsset(params.skillAssets, dependency, currentInstancePath)
|
|
284
|
-
if (resolvedAsset != null && resolvedAsset.resolvedBy !== HOME_BRIDGE_RESOLVED_BY) {
|
|
285
|
-
removeSupersededHomeBridgeSkill(resolvedAsset.displayName)
|
|
286
|
-
}
|
|
287
|
-
return resolvedAsset
|
|
288
|
-
}
|
|
289
|
-
|
|
290
202
|
const addAsset = async (asset: SkillAsset): Promise<void> => {
|
|
291
203
|
if (params.excludedIds?.has(asset.id)) return
|
|
292
204
|
if (seen.has(asset.id)) return
|
|
@@ -311,25 +223,15 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
|
|
|
311
223
|
continue
|
|
312
224
|
}
|
|
313
225
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const dependencyAsset = await installDependencyAsset(dependency, asset.instancePath).catch((error: unknown) => {
|
|
318
|
-
if (
|
|
319
|
-
localOrBridgedDependency != null &&
|
|
320
|
-
dependency.source == null
|
|
321
|
-
) {
|
|
322
|
-
return localOrBridgedDependency
|
|
323
|
-
}
|
|
324
|
-
throw error
|
|
325
|
-
}) ?? (
|
|
326
|
-
dependency.source == null
|
|
327
|
-
? localOrBridgedDependency
|
|
328
|
-
: undefined
|
|
329
|
-
)
|
|
226
|
+
const dependencyAsset = dependency.source == null
|
|
227
|
+
? localOrBridgedDependency
|
|
228
|
+
: undefined
|
|
330
229
|
|
|
331
230
|
if (dependencyAsset == null) {
|
|
332
|
-
throw new Error(
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Skill dependency ${dependency.ref} declared by ${asset.displayName} is missing. ` +
|
|
233
|
+
'Run vf skills install or vf skills update to materialize project skill dependencies.'
|
|
234
|
+
)
|
|
333
235
|
}
|
|
334
236
|
await addAsset(dependencyAsset)
|
|
335
237
|
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { access, copyFile, lstat, mkdir, readdir } from 'node:fs/promises'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
|
|
5
|
-
import { withDirectoryInstallLock } from '@vibe-forge/utils/install-lock'
|
|
6
|
-
import { resolveProjectSharedCachePath } from '@vibe-forge/utils/project-cache-path'
|
|
7
|
-
import { toSkillSlug } from '@vibe-forge/utils/skills-cli'
|
|
8
|
-
|
|
9
|
-
const toCacheSegment = (value: string) => (
|
|
10
|
-
value
|
|
11
|
-
.trim()
|
|
12
|
-
.toLowerCase()
|
|
13
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
14
|
-
.replace(/^-+|-+$/g, '') || 'default'
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
export const pathExists = async (targetPath: string) => {
|
|
18
|
-
try {
|
|
19
|
-
await access(targetPath)
|
|
20
|
-
return true
|
|
21
|
-
} catch {
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const withInstallLock = async <T>(lockDir: string, callback: () => Promise<T>) => {
|
|
27
|
-
try {
|
|
28
|
-
return await withDirectoryInstallLock({ lockDir }, callback)
|
|
29
|
-
} catch (error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
31
|
-
throw new Error(
|
|
32
|
-
message.replace('Timed out waiting for install lock', 'Timed out waiting for skill dependency install lock')
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const copyRegularFiles = async (sourceDir: string, targetDir: string) => {
|
|
38
|
-
let fileCount = 0
|
|
39
|
-
const entries = await readdir(sourceDir, { withFileTypes: true })
|
|
40
|
-
|
|
41
|
-
await mkdir(targetDir, { recursive: true })
|
|
42
|
-
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
const sourcePath = resolve(sourceDir, entry.name)
|
|
45
|
-
const targetPath = resolve(targetDir, entry.name)
|
|
46
|
-
const stat = await lstat(sourcePath)
|
|
47
|
-
|
|
48
|
-
if (stat.isDirectory()) {
|
|
49
|
-
fileCount += await copyRegularFiles(sourcePath, targetPath)
|
|
50
|
-
continue
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!stat.isFile()) continue
|
|
54
|
-
|
|
55
|
-
await mkdir(dirname(targetPath), { recursive: true })
|
|
56
|
-
await copyFile(sourcePath, targetPath)
|
|
57
|
-
fileCount += 1
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return fileCount
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const pickSearchResult = <T extends { skill: string }>(
|
|
64
|
-
results: T[],
|
|
65
|
-
name: string
|
|
66
|
-
) => {
|
|
67
|
-
const slug = toSkillSlug(name)
|
|
68
|
-
return results.find(result => (
|
|
69
|
-
result.skill === name ||
|
|
70
|
-
toSkillSlug(result.skill) === slug
|
|
71
|
-
)) ?? results[0]
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const buildInstallDir = (params: {
|
|
75
|
-
cwd: string
|
|
76
|
-
registry?: string
|
|
77
|
-
skill: string
|
|
78
|
-
source: string
|
|
79
|
-
version?: string
|
|
80
|
-
}) => {
|
|
81
|
-
const registry = params.registry ?? 'default'
|
|
82
|
-
return resolveProjectSharedCachePath(
|
|
83
|
-
params.cwd,
|
|
84
|
-
process.env,
|
|
85
|
-
'skill-dependencies',
|
|
86
|
-
'skills-cli',
|
|
87
|
-
toCacheSegment('skills'),
|
|
88
|
-
toCacheSegment('latest'),
|
|
89
|
-
toCacheSegment(registry),
|
|
90
|
-
...params.source.split('/').map(toCacheSegment),
|
|
91
|
-
toCacheSegment(params.version ?? 'latest'),
|
|
92
|
-
toCacheSegment(params.skill)
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { mkdir, rename, rm } from 'node:fs/promises'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
|
|
5
|
-
import type { Config } from '@vibe-forge/types'
|
|
6
|
-
import { isObjectSkillsConfig, resolveSkillsRegistry } from '@vibe-forge/utils'
|
|
7
|
-
import { findSkillsCli, installSkillsCliRefToTemp, installSkillsCliSkillToTemp } from '@vibe-forge/utils/skills-cli'
|
|
8
|
-
|
|
9
|
-
import type { NormalizedSkillDependency } from './skill-dependencies'
|
|
10
|
-
import {
|
|
11
|
-
buildInstallDir,
|
|
12
|
-
copyRegularFiles,
|
|
13
|
-
pathExists,
|
|
14
|
-
pickSearchResult,
|
|
15
|
-
withInstallLock
|
|
16
|
-
} from './skills-cli-dependency-helpers'
|
|
17
|
-
|
|
18
|
-
const resolveAutoDownloadDependenciesEnabled = (
|
|
19
|
-
projectConfig: Config | undefined,
|
|
20
|
-
userConfig: Config | undefined
|
|
21
|
-
) => (
|
|
22
|
-
(isObjectSkillsConfig(userConfig?.skills) ? userConfig.skills.autoDownloadDependencies : undefined) ??
|
|
23
|
-
(isObjectSkillsConfig(projectConfig?.skills) ? projectConfig.skills.autoDownloadDependencies : undefined) ??
|
|
24
|
-
true
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
export const installSkillsCliDependency = async (params: {
|
|
28
|
-
cwd: string
|
|
29
|
-
configs: [Config?, Config?]
|
|
30
|
-
dependency: NormalizedSkillDependency
|
|
31
|
-
}) => {
|
|
32
|
-
const [projectConfig, userConfig] = params.configs
|
|
33
|
-
const autoDownloadDependenciesEnabled = resolveAutoDownloadDependenciesEnabled(projectConfig, userConfig)
|
|
34
|
-
const defaultRegistry = resolveSkillsRegistry(params.configs[1]?.skills) ??
|
|
35
|
-
resolveSkillsRegistry(params.configs[0]?.skills)
|
|
36
|
-
const registry = params.dependency.registry ?? defaultRegistry
|
|
37
|
-
const resolvedTarget = await (async () => {
|
|
38
|
-
if (params.dependency.source != null) {
|
|
39
|
-
return {
|
|
40
|
-
skill: params.dependency.name,
|
|
41
|
-
source: params.dependency.source
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!autoDownloadDependenciesEnabled) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Skill dependency automatic downloads are disabled; cannot resolve ${params.dependency.ref} without a source`
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return await (async () => {
|
|
52
|
-
const searchResults = await findSkillsCli({
|
|
53
|
-
registry,
|
|
54
|
-
query: params.dependency.name
|
|
55
|
-
})
|
|
56
|
-
const selected = pickSearchResult(searchResults, params.dependency.name)
|
|
57
|
-
if (selected == null) {
|
|
58
|
-
throw new Error(`Skill ${params.dependency.name} was not found by the skills CLI search.`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
installRef: selected.installRef,
|
|
63
|
-
skill: selected.skill,
|
|
64
|
-
source: selected.source
|
|
65
|
-
}
|
|
66
|
-
})()
|
|
67
|
-
})()
|
|
68
|
-
|
|
69
|
-
const installDir = buildInstallDir({
|
|
70
|
-
cwd: params.cwd,
|
|
71
|
-
registry,
|
|
72
|
-
skill: resolvedTarget.skill,
|
|
73
|
-
source: resolvedTarget.source,
|
|
74
|
-
version: params.dependency.version
|
|
75
|
-
})
|
|
76
|
-
const skillPath = resolve(installDir, 'SKILL.md')
|
|
77
|
-
|
|
78
|
-
return await withInstallLock(`${installDir}.lock`, async () => {
|
|
79
|
-
if (await pathExists(skillPath)) {
|
|
80
|
-
return {
|
|
81
|
-
installDir,
|
|
82
|
-
skillPath
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!autoDownloadDependenciesEnabled) {
|
|
87
|
-
throw new Error(`Skill dependency automatic downloads are disabled; cache not found for ${params.dependency.ref}`)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const tempInstallDir = `${installDir}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
91
|
-
await rm(tempInstallDir, { recursive: true, force: true })
|
|
92
|
-
await mkdir(tempInstallDir, { recursive: true })
|
|
93
|
-
|
|
94
|
-
const installResult = 'installRef' in resolvedTarget
|
|
95
|
-
? params.dependency.version == null
|
|
96
|
-
? await installSkillsCliRefToTemp({
|
|
97
|
-
installRef: resolvedTarget.installRef,
|
|
98
|
-
registry
|
|
99
|
-
})
|
|
100
|
-
: await installSkillsCliSkillToTemp({
|
|
101
|
-
registry,
|
|
102
|
-
skill: resolvedTarget.skill,
|
|
103
|
-
source: resolvedTarget.source,
|
|
104
|
-
version: params.dependency.version
|
|
105
|
-
})
|
|
106
|
-
: await installSkillsCliSkillToTemp({
|
|
107
|
-
registry,
|
|
108
|
-
skill: resolvedTarget.skill,
|
|
109
|
-
source: resolvedTarget.source,
|
|
110
|
-
version: params.dependency.version
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await copyRegularFiles(installResult.installedSkill.sourcePath, tempInstallDir)
|
|
115
|
-
if (!await pathExists(resolve(tempInstallDir, 'SKILL.md'))) {
|
|
116
|
-
throw new Error(`Skill dependency ${params.dependency.ref} did not include SKILL.md`)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
await rm(installDir, { recursive: true, force: true })
|
|
120
|
-
await rename(tempInstallDir, installDir)
|
|
121
|
-
} catch (error) {
|
|
122
|
-
await rm(tempInstallDir, { recursive: true, force: true })
|
|
123
|
-
throw error
|
|
124
|
-
} finally {
|
|
125
|
-
await rm(installResult.tempDir, { recursive: true, force: true })
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
installDir,
|
|
130
|
-
skillPath
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
}
|