@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.
@@ -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
 
@@ -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 { access, mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises'
3
- import os from 'node:os'
4
- import path, { join } from 'node:path'
2
+ import { join } from 'node:path'
5
3
 
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
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('skills CLI dependency resolution', () => {
29
- let installWorkspace: string
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
- afterEach(async () => {
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('Skill dependency automatic downloads are disabled; cache not found')
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('blocks bare-name dependency searches when auto downloads are disabled', async () => {
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/app-builder/SKILL.md'),
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('parses registry and version from dependency specs', async () => {
108
+ it('loads plugin dependencies from .ai/skills/.plugins through the lockfile', async () => {
252
109
  const workspace = await createWorkspace()
253
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
254
- await mkdir(installedSkillDir, { recursive: true })
255
- await writeFile(
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: app-builder',
274
- 'description: Build apps',
114
+ 'name: review-helper',
115
+ 'description: Review helper',
275
116
  'dependencies:',
276
- ' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
117
+ ' - shared-runtime',
277
118
  '---',
278
- 'Build the app.'
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
- expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
299
- registry: 'https://registry.example.com',
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/lynx-miniapp/SKILL.md'),
127
+ join(workspace, '.ai/skills.lock.yaml'),
326
128
  [
327
- '---',
328
- 'name: lynx-miniapp',
329
- 'description: lynx 调试使用',
330
- 'dependencies:',
331
- ' - https://registry.example.com@example-source/lynx/skills@lynx-cat@latest',
332
- '---',
333
- '这是一个测试的 lynx 调试技能'
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: [undefined, undefined],
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: ['lynx-miniapp']
161
+ include: ['review/review-helper']
356
162
  }
357
163
  }
358
164
  })
359
165
 
360
- expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
361
- registry: 'https://registry.example.com',
362
- skill: 'lynx-cat',
363
- source: 'example-source/lynx/skills',
364
- version: 'latest'
365
- })
366
- await expect(pathExists(lockDir)).resolves.toBe(false)
367
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['lynx-cat', 'lynx-miniapp'])
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.1",
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/config": "3.2.2",
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"
@@ -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
- : assets
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
- : bundle.skills
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 { readFile } from 'node:fs/promises'
3
-
4
- import fm from 'front-matter'
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
- // Keep registry-backed dependencies ahead of home-bridge skills. We only
315
- // fall back to a bridged home skill for plain-name dependencies when the
316
- // registry path is unavailable.
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(`Failed to resolve skill dependency ${dependency.ref} declared by ${asset.displayName}`)
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
- }