@vibe-forge/workspace-assets 3.2.1 → 3.2.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,36 +57,25 @@ 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('prompts install command when an explicitly selected configured skill is missing', async () => {
161
68
  const workspace = await createWorkspace()
162
-
163
- await writeDocument(
164
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
165
- [
166
- '---',
167
- 'name: app-builder',
168
- 'description: Build apps',
169
- 'dependencies:',
170
- ' - frontend-design',
171
- '---',
172
- 'Build the app.'
173
- ].join('\n')
174
- )
175
-
176
69
  const bundle = await resolveWorkspaceAssetBundle({
177
70
  cwd: workspace,
178
71
  configs: [{
179
- skills: {
180
- autoDownloadDependencies: false
181
- }
72
+ skills: [
73
+ {
74
+ name: 'design-review',
75
+ source: 'example-source/default/public',
76
+ rename: 'internal-review'
77
+ }
78
+ ]
182
79
  }, undefined],
183
80
  useDefaultVibeForgeMcpServer: false
184
81
  })
@@ -188,27 +85,18 @@ describe('skills CLI dependency resolution', () => {
188
85
  bundle,
189
86
  options: {
190
87
  skills: {
191
- include: ['app-builder']
88
+ include: ['internal-review']
192
89
  }
193
90
  }
194
- })).rejects.toThrow(
195
- 'Skill dependency automatic downloads are disabled; cannot resolve frontend-design without a source'
196
- )
91
+ })).rejects.toThrow('Run `vf skills install internal-review` or `vf skills install`')
197
92
 
198
93
  expect(mocks.findSkillsCli).not.toHaveBeenCalled()
199
94
  expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
200
95
  expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
201
96
  })
202
97
 
203
- it('reuses source-qualified dependency caches when auto downloads are disabled', async () => {
98
+ it('uses project-materialized dependencies from .ai/skills', async () => {
204
99
  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
100
  await writeDocument(
213
101
  join(workspace, '.ai/skills/app-builder/SKILL.md'),
214
102
  [
@@ -216,23 +104,22 @@ describe('skills CLI dependency resolution', () => {
216
104
  'name: app-builder',
217
105
  'description: Build apps',
218
106
  'dependencies:',
219
- ' - anthropics/skills@frontend-design',
107
+ ' - frontend-design',
220
108
  '---',
221
109
  'Build the app.'
222
110
  ].join('\n')
223
111
  )
112
+ await writeDocument(
113
+ join(workspace, '.ai/skills/frontend-design/SKILL.md'),
114
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
115
+ )
224
116
 
225
117
  const bundle = await resolveWorkspaceAssetBundle({
226
118
  cwd: workspace,
227
- configs: [{
228
- skills: {
229
- autoDownloadDependencies: false
230
- }
231
- }, undefined],
119
+ configs: [undefined, undefined],
232
120
  useDefaultVibeForgeMcpServer: false
233
121
  })
234
-
235
- await buildAdapterAssetPlan({
122
+ const plan = await buildAdapterAssetPlan({
236
123
  adapter: 'opencode',
237
124
  bundle,
238
125
  options: {
@@ -242,128 +129,79 @@ describe('skills CLI dependency resolution', () => {
242
129
  }
243
130
  })
244
131
 
132
+ expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
133
+ 'skills/app-builder',
134
+ 'skills/frontend-design'
135
+ ])
245
136
  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
137
  })
250
138
 
251
- it('parses registry and version from dependency specs', async () => {
139
+ it('loads plugin dependencies from .ai/skills/.plugins through the lockfile', async () => {
252
140
  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
- [
141
+ await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
142
+ 'package.json': JSON.stringify({ name: '@vibe-forge/plugin-review', version: '1.0.0' }, null, 2),
143
+ 'skills/review-helper/SKILL.md': [
272
144
  '---',
273
- 'name: app-builder',
274
- 'description: Build apps',
145
+ 'name: review-helper',
146
+ 'description: Review helper',
275
147
  'dependencies:',
276
- ' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
148
+ ' - shared-runtime',
277
149
  '---',
278
- 'Build the app.'
150
+ 'Review code.'
279
151
  ].join('\n')
280
- )
281
-
282
- const bundle = await resolveWorkspaceAssetBundle({
283
- cwd: workspace,
284
- configs: [undefined, undefined],
285
- useDefaultVibeForgeMcpServer: false
286
152
  })
287
-
288
- await buildAdapterAssetPlan({
289
- adapter: 'opencode',
290
- bundle,
291
- options: {
292
- skills: {
293
- include: ['app-builder']
294
- }
295
- }
296
- })
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'
153
+ await writeDocument(
154
+ join(workspace, '.ai/skills/.plugins/review/shared-runtime/SKILL.md'),
155
+ '---\nname: shared-runtime\ndescription: Shared runtime\n---\nShared plugin dependency.\n'
313
156
  )
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
157
  await writeDocument(
325
- join(workspace, '.ai/skills/lynx-miniapp/SKILL.md'),
158
+ join(workspace, '.ai/skills.lock.yaml'),
326
159
  [
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 调试技能'
160
+ 'version: 1',
161
+ 'pluginSkills:',
162
+ ' review/shared-runtime:',
163
+ ' name: shared-runtime',
164
+ ' requested: false',
165
+ ' pluginInstance: review',
166
+ ' pluginInstancePath: "0"',
167
+ ' installPath: .ai/skills/.plugins/review/shared-runtime',
168
+ ' dependencyOf:',
169
+ ' - plugin:review/review-helper',
170
+ ' source: vendor/shared-skills',
171
+ ' version: 1.0.0',
172
+ ' hash: sha256:test',
173
+ ' installedAt: "2026-05-13T00:00:00.000Z"'
334
174
  ].join('\n')
335
175
  )
336
176
 
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
177
  const bundle = await resolveWorkspaceAssetBundle({
345
178
  cwd: workspace,
346
- configs: [undefined, undefined],
179
+ configs: [{
180
+ plugins: [{
181
+ id: '@vibe-forge/plugin-review',
182
+ scope: 'review'
183
+ }]
184
+ }, undefined],
347
185
  useDefaultVibeForgeMcpServer: false
348
186
  })
349
-
350
- await buildAdapterAssetPlan({
187
+ const plan = await buildAdapterAssetPlan({
351
188
  adapter: 'opencode',
352
189
  bundle,
353
190
  options: {
354
191
  skills: {
355
- include: ['lynx-miniapp']
192
+ include: ['review/review-helper']
356
193
  }
357
194
  }
358
195
  })
359
196
 
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'])
197
+ expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual([
198
+ 'review/review-helper',
199
+ 'review/shared-runtime'
200
+ ])
201
+ expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
202
+ 'skills/review__review-helper',
203
+ 'skills/review__shared-runtime'
204
+ ])
205
+ expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
368
206
  })
369
207
  })
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.1",
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/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"
32
+ "@vibe-forge/config": "3.2.3-alpha.1",
33
+ "@vibe-forge/utils": "3.2.3-alpha.1",
34
+ "@vibe-forge/types": "3.2.3-alpha.1",
35
+ "@vibe-forge/definition-core": "3.2.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
 
@@ -10,10 +10,11 @@ import {
10
10
  } from '@vibe-forge/config'
11
11
  import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
12
12
  import {
13
- isObjectSkillsConfig,
13
+ readProjectSkillsLockfile,
14
14
  resolveProjectAiBaseDir,
15
15
  resolveProjectAiEntitiesDir,
16
- resolveRelativePath
16
+ resolveRelativePath,
17
+ resolveSkillsHomeBridge
17
18
  } from '@vibe-forge/utils'
18
19
  import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
19
20
  import {
@@ -33,8 +34,9 @@ import {
33
34
  resolveSkillIdentifier,
34
35
  resolveSpecIdentifier
35
36
  } from '@vibe-forge/definition-core'
36
- import { ensureConfiguredProjectSkills } from './configured-skills'
37
+ import { warnMissingConfiguredProjectSkills } 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'>
@@ -152,15 +154,15 @@ const resolveRealHomeDir = (env: NodeJS.ProcessEnv) => {
152
154
 
153
155
  const warnInvalidHomeSkillRoot = (root: string) => {
154
156
  console.warn(
155
- `[vibe-forge] Ignoring invalid skills.homeBridge root "${root}". ` +
157
+ `[vibe-forge] Ignoring invalid skillsMeta.homeBridge root "${root}". ` +
156
158
  'Use an absolute path or a path starting with "~".'
157
159
  )
158
160
  }
159
161
 
160
162
  const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
161
163
  const [config, userConfig] = configs
162
- const projectHomeBridge = isObjectSkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
163
- const userHomeBridge = isObjectSkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
164
+ const projectHomeBridge = resolveSkillsHomeBridge(config)
165
+ const userHomeBridge = resolveSkillsHomeBridge(userConfig)
164
166
 
165
167
  return {
166
168
  enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
@@ -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',
@@ -535,6 +571,7 @@ export async function collectWorkspaceAssets(params: {
535
571
  syncConfiguredSkills?: boolean
536
572
  updateConfiguredSkills?: boolean
537
573
  useDefaultVibeForgeMcpServer?: boolean
574
+ warnMissingConfiguredSkills?: boolean
538
575
  }): Promise<{
539
576
  assets: WorkspaceAsset[]
540
577
  configs: [Config?, Config?]
@@ -552,10 +589,15 @@ export async function collectWorkspaceAssets(params: {
552
589
  workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
553
590
  }> {
554
591
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
555
- if (params.syncConfiguredSkills === true) {
556
- await ensureConfiguredProjectSkills({
592
+ if (params.syncConfiguredSkills === true || params.updateConfiguredSkills === true) {
593
+ console.warn(
594
+ '[vibe-forge] Runtime skill install/update is disabled. ' +
595
+ 'Run `vf skills install` or `vf skills update` before starting the session.'
596
+ )
597
+ }
598
+ if (params.warnMissingConfiguredSkills === true) {
599
+ await warnMissingConfiguredProjectSkills({
557
600
  configs: [config, userConfig],
558
- updateInstalledSkills: params.updateConfiguredSkills,
559
601
  workspaceFolder: params.cwd
560
602
  })
561
603
  }
@@ -578,6 +620,7 @@ export async function collectWorkspaceAssets(params: {
578
620
  ])
579
621
  const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
580
622
  const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
623
+ const pluginDependencySkillDocs = await scanPluginDependencySkillDocuments(params.cwd, flattenedPluginInstances)
581
624
  const pluginOverlayScans = await Promise.all(
582
625
  flattenedPluginInstances.map(instance => scanInstanceOpenCodeOverlays(instance))
583
626
  )
@@ -631,6 +674,16 @@ export async function collectWorkspaceAssets(params: {
631
674
  await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
632
675
  await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
633
676
  }
677
+ for (const entry of pluginDependencySkillDocs) {
678
+ await pushDocumentAssets(
679
+ 'skill',
680
+ [entry.path],
681
+ 'plugin',
682
+ entry.instance,
683
+ undefined,
684
+ PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
685
+ )
686
+ }
634
687
  await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
635
688
 
636
689
  const skills = mergeSkillAssets(skillAssets)
package/src/bundle.ts CHANGED
@@ -11,6 +11,7 @@ export async function resolveWorkspaceAssetBundle(params: {
11
11
  syncConfiguredSkills?: boolean
12
12
  updateConfiguredSkills?: boolean
13
13
  useDefaultVibeForgeMcpServer?: boolean
14
+ warnMissingConfiguredSkills?: boolean
14
15
  }): Promise<WorkspaceAssetBundle> {
15
16
  const collected = await collectWorkspaceAssets(params)
16
17
 
@@ -3,15 +3,13 @@ import process from 'node:process'
3
3
 
4
4
  import type { Config, ConfiguredSkillInstallConfig } from '@vibe-forge/types'
5
5
  import {
6
- installProjectSkill,
7
6
  normalizeProjectSkillInstall,
8
7
  resolveConfiguredSkillInstalls as resolveDeclaredConfiguredSkillInstalls,
9
- resolveProjectAiPath,
10
- resolveSkillsRegistry
8
+ resolveProjectAiPath
11
9
  } from '@vibe-forge/utils'
12
10
  import type { NormalizedProjectSkillInstall } from '@vibe-forge/utils'
13
11
 
14
- const resolveConfiguredSkillInstalls = (configs: [Config?, Config?]) => (
12
+ export const resolveConfiguredProjectSkillInstalls = (configs: [Config?, Config?]) => (
15
13
  [
16
14
  ...resolveDeclaredConfiguredSkillInstalls(configs[0]?.skills),
17
15
  ...resolveDeclaredConfiguredSkillInstalls(configs[1]?.skills)
@@ -29,7 +27,7 @@ const pathExists = async (targetPath: string) => {
29
27
  }
30
28
  }
31
29
 
32
- const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
30
+ export const ensureUniqueConfiguredSkillTargets = (skills: NormalizedProjectSkillInstall[]) => {
33
31
  const seen = new Map<string, string>()
34
32
 
35
33
  for (const skill of skills) {
@@ -43,22 +41,18 @@ const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
43
41
  }
44
42
  }
45
43
 
46
- export const ensureConfiguredProjectSkills = async (params: {
44
+ export const findMissingConfiguredProjectSkills = async (params: {
47
45
  configs: [Config?, Config?]
48
- updateInstalledSkills?: boolean
49
46
  workspaceFolder: string
50
47
  }) => {
51
- const defaultRegistry = resolveSkillsRegistry(params.configs[1]?.skills) ??
52
- resolveSkillsRegistry(params.configs[0]?.skills)
53
- const installs = resolveConfiguredSkillInstalls(params.configs)
48
+ const installs = resolveConfiguredProjectSkillInstalls(params.configs)
54
49
  if (installs.length === 0) {
55
50
  return []
56
51
  }
57
52
 
58
- ensureUniqueTargets(installs)
59
-
60
- const ensured: Array<{ dirName: string; skillPath: string }> = []
53
+ ensureUniqueConfiguredSkillTargets(installs)
61
54
 
55
+ const missing: NormalizedProjectSkillInstall[] = []
62
56
  for (const skill of installs) {
63
57
  const skillPath = resolveProjectAiPath(
64
58
  params.workspaceFolder,
@@ -67,23 +61,23 @@ export const ensureConfiguredProjectSkills = async (params: {
67
61
  skill.targetDirName,
68
62
  'SKILL.md'
69
63
  )
70
- if (params.updateInstalledSkills !== true && await pathExists(skillPath)) {
71
- ensured.push({
72
- dirName: skill.targetDirName,
73
- skillPath
74
- })
75
- continue
76
- }
77
-
78
- ensured.push(
79
- await installProjectSkill({
80
- force: true,
81
- registry: defaultRegistry,
82
- skill,
83
- workspaceFolder: params.workspaceFolder
84
- })
85
- )
64
+ if (!await pathExists(skillPath)) missing.push(skill)
86
65
  }
87
66
 
88
- return ensured
67
+ return missing
68
+ }
69
+
70
+ export const warnMissingConfiguredProjectSkills = async (params: {
71
+ configs: [Config?, Config?]
72
+ workspaceFolder: string
73
+ }) => {
74
+ const missing = await findMissingConfiguredProjectSkills(params)
75
+ if (missing.length === 0) return []
76
+
77
+ const names = missing.map(skill => skill.targetName).join(', ')
78
+ console.warn(
79
+ `[vibe-forge] Declared skills are not installed: ${names}. ` +
80
+ 'Run `vf skills install` to install all declared skills, or `vf skills install <name>` for one skill.'
81
+ )
82
+ return missing
89
83
  }
@@ -0,0 +1 @@
1
+ export const PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY = 'plugin-skill-dependency-lock'