@vibe-forge/workspace-assets 3.2.2-alpha.0 → 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.
@@ -2,7 +2,6 @@
2
2
  import { join } from 'node:path'
3
3
  import process from 'node:process'
4
4
 
5
- import { readFile } from 'node:fs/promises'
6
5
  import { afterEach, describe, expect, it, vi } from 'vitest'
7
6
 
8
7
  const skillsCliMocks = vi.hoisted(() => ({
@@ -27,6 +26,7 @@ import { createWorkspace, installPluginPackage, writeDocument } from './test-hel
27
26
 
28
27
  afterEach(() => {
29
28
  vi.clearAllMocks()
29
+ vi.restoreAllMocks()
30
30
  vi.unstubAllGlobals()
31
31
  })
32
32
 
@@ -291,7 +291,7 @@ describe('resolveWorkspaceAssetBundle', () => {
291
291
 
292
292
  expect(bundle.skills).toEqual([])
293
293
  expect(warnSpy).toHaveBeenCalledWith(
294
- expect.stringContaining('Ignoring invalid skills.homeBridge root "./team-skills"')
294
+ expect.stringContaining('Ignoring invalid skillsMeta.homeBridge root "./team-skills"')
295
295
  )
296
296
  } finally {
297
297
  warnSpy.mockRestore()
@@ -386,22 +386,9 @@ describe('resolveWorkspaceAssetBundle', () => {
386
386
  }))
387
387
  })
388
388
 
389
- it('installs configured project skills before bundle resolution and rewrites renamed skill names', async () => {
389
+ it('does not install configured project skills during bundle resolution and warns when requested', async () => {
390
390
  const workspace = await createWorkspace()
391
- const tempInstallDir = join(workspace, '.tmp-configured-install')
392
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
393
- await writeDocument(
394
- join(installedSkillDir, 'SKILL.md'),
395
- '---\nname: design-review\ndescription: Review design work\n---\nReview the UI implementation.\n'
396
- )
397
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
398
- tempDir: tempInstallDir,
399
- installedSkill: {
400
- dirName: 'design-review',
401
- name: 'design-review',
402
- sourcePath: installedSkillDir
403
- }
404
- })
391
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
405
392
 
406
393
  const bundle = await resolveWorkspaceAssetBundle({
407
394
  cwd: workspace,
@@ -414,22 +401,16 @@ describe('resolveWorkspaceAssetBundle', () => {
414
401
  }
415
402
  ]
416
403
  }, undefined],
417
- syncConfiguredSkills: true,
404
+ warnMissingConfiguredSkills: true,
418
405
  useDefaultVibeForgeMcpServer: false
419
406
  })
420
407
 
421
- expect(bundle.skills.map(asset => asset.name)).toContain('internal-review')
422
- expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
423
- config: undefined,
424
- skill: 'design-review',
425
- source: 'example-source/default/public'
426
- })
427
- await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
428
- 'name: internal-review'
429
- )
408
+ expect(bundle.skills.map(asset => asset.name)).not.toContain('internal-review')
409
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
410
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('Declared skills are not installed: internal-review'))
430
411
  })
431
412
 
432
- it('skips configured skill reinstalls unless updateConfiguredSkills is enabled', async () => {
413
+ it('loads installed configured skills without attempting runtime updates', async () => {
433
414
  const workspace = await createWorkspace()
434
415
  await writeDocument(
435
416
  join(workspace, '.ai/skills/internal-review/SKILL.md'),
@@ -447,48 +428,12 @@ describe('resolveWorkspaceAssetBundle', () => {
447
428
  }
448
429
  ]
449
430
  }, undefined],
450
- syncConfiguredSkills: true,
431
+ warnMissingConfiguredSkills: true,
451
432
  useDefaultVibeForgeMcpServer: false
452
433
  })
453
434
 
454
435
  expect(skippedBundle.skills.map(asset => asset.name)).toContain('internal-review')
455
436
  expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
456
-
457
- const tempInstallDir = join(workspace, '.tmp-configured-update')
458
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
459
- await writeDocument(
460
- join(installedSkillDir, 'SKILL.md'),
461
- '---\nname: design-review\ndescription: Updated skill\n---\nUpdated content.\n'
462
- )
463
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValueOnce({
464
- tempDir: tempInstallDir,
465
- installedSkill: {
466
- dirName: 'design-review',
467
- name: 'design-review',
468
- sourcePath: installedSkillDir
469
- }
470
- })
471
-
472
- await resolveWorkspaceAssetBundle({
473
- cwd: workspace,
474
- configs: [{
475
- skills: [
476
- {
477
- name: 'design-review',
478
- source: 'example-source/default/public',
479
- rename: 'internal-review'
480
- }
481
- ]
482
- }, undefined],
483
- syncConfiguredSkills: true,
484
- updateConfiguredSkills: true,
485
- useDefaultVibeForgeMcpServer: false
486
- })
487
-
488
- expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledTimes(1)
489
- await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
490
- 'Updated content.'
491
- )
492
437
  })
493
438
 
494
439
  it('loads workspace entities from the env-configured entities dir', async () => {
@@ -64,6 +64,37 @@ describe('materialized skill dependency resolution', () => {
64
64
  expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
65
65
  })
66
66
 
67
+ it('prompts install command when an explicitly selected configured skill is missing', async () => {
68
+ const workspace = await createWorkspace()
69
+ const bundle = await resolveWorkspaceAssetBundle({
70
+ cwd: workspace,
71
+ configs: [{
72
+ skills: [
73
+ {
74
+ name: 'design-review',
75
+ source: 'example-source/default/public',
76
+ rename: 'internal-review'
77
+ }
78
+ ]
79
+ }, undefined],
80
+ useDefaultVibeForgeMcpServer: false
81
+ })
82
+
83
+ await expect(buildAdapterAssetPlan({
84
+ adapter: 'opencode',
85
+ bundle,
86
+ options: {
87
+ skills: {
88
+ include: ['internal-review']
89
+ }
90
+ }
91
+ })).rejects.toThrow('Run `vf skills install internal-review` or `vf skills install`')
92
+
93
+ expect(mocks.findSkillsCli).not.toHaveBeenCalled()
94
+ expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
95
+ expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
96
+ })
97
+
67
98
  it('uses project-materialized dependencies from .ai/skills', async () => {
68
99
  const workspace = await createWorkspace()
69
100
  await writeDocument(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "3.2.2-alpha.0",
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/config": "3.2.3-alpha.0",
33
- "@vibe-forge/types": "3.2.3-alpha.0",
34
- "@vibe-forge/definition-core": "3.2.0",
35
- "@vibe-forge/utils": "3.2.3-alpha.0"
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"
@@ -10,11 +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,
14
13
  readProjectSkillsLockfile,
15
14
  resolveProjectAiBaseDir,
16
15
  resolveProjectAiEntitiesDir,
17
- resolveRelativePath
16
+ resolveRelativePath,
17
+ resolveSkillsHomeBridge
18
18
  } from '@vibe-forge/utils'
19
19
  import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
20
20
  import {
@@ -34,7 +34,7 @@ import {
34
34
  resolveSkillIdentifier,
35
35
  resolveSpecIdentifier
36
36
  } from '@vibe-forge/definition-core'
37
- import { ensureConfiguredProjectSkills } from './configured-skills'
37
+ import { warnMissingConfiguredProjectSkills } from './configured-skills'
38
38
  import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
39
39
  import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
40
40
  import { resolveConfiguredWorkspaceAssets } from './workspaces'
@@ -154,15 +154,15 @@ const resolveRealHomeDir = (env: NodeJS.ProcessEnv) => {
154
154
 
155
155
  const warnInvalidHomeSkillRoot = (root: string) => {
156
156
  console.warn(
157
- `[vibe-forge] Ignoring invalid skills.homeBridge root "${root}". ` +
157
+ `[vibe-forge] Ignoring invalid skillsMeta.homeBridge root "${root}". ` +
158
158
  'Use an absolute path or a path starting with "~".'
159
159
  )
160
160
  }
161
161
 
162
162
  const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
163
163
  const [config, userConfig] = configs
164
- const projectHomeBridge = isObjectSkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
165
- const userHomeBridge = isObjectSkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
164
+ const projectHomeBridge = resolveSkillsHomeBridge(config)
165
+ const userHomeBridge = resolveSkillsHomeBridge(userConfig)
166
166
 
167
167
  return {
168
168
  enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
@@ -571,6 +571,7 @@ export async function collectWorkspaceAssets(params: {
571
571
  syncConfiguredSkills?: boolean
572
572
  updateConfiguredSkills?: boolean
573
573
  useDefaultVibeForgeMcpServer?: boolean
574
+ warnMissingConfiguredSkills?: boolean
574
575
  }): Promise<{
575
576
  assets: WorkspaceAsset[]
576
577
  configs: [Config?, Config?]
@@ -588,10 +589,15 @@ export async function collectWorkspaceAssets(params: {
588
589
  workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
589
590
  }> {
590
591
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
591
- if (params.syncConfiguredSkills === true) {
592
- 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({
593
600
  configs: [config, userConfig],
594
- updateInstalledSkills: params.updateConfiguredSkills,
595
601
  workspaceFolder: params.cwd
596
602
  })
597
603
  }
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
  }
@@ -29,7 +29,7 @@ import {
29
29
  resolveEntityInheritance,
30
30
  resolveExcludedSkillRefs,
31
31
  resolveIncludedSkillRefs,
32
- resolveNamedAssets,
32
+ resolveNamedSkillAssets,
33
33
  resolvePluginOverlay,
34
34
  resolveRuleSelection,
35
35
  resolveSelectedSkillAssetsWithDependencies,
@@ -158,10 +158,10 @@ export async function resolvePromptAssetSelection(params: {
158
158
  const includedAssets = skillSelection == null
159
159
  ? []
160
160
  : includedRefs != null
161
- ? (includedRefs.length > 0 ? resolveNamedAssets(effectiveBundle.skills, includedRefs, targetInstancePath) : [])
161
+ ? (includedRefs.length > 0 ? resolveNamedSkillAssets(effectiveBundle, includedRefs, targetInstancePath) : [])
162
162
  : effectiveBundle.skills
163
163
  const excludedIds = new Set(
164
- resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
164
+ resolveNamedSkillAssets(effectiveBundle, excludedRefs, targetInstancePath).map(asset => asset.id)
165
165
  )
166
166
 
167
167
  const expandedTargetSkills = await expandSkillAssetDependenciesWithRemoteResolution({
@@ -15,7 +15,7 @@ import type {
15
15
  WorkspaceMcpSelection,
16
16
  WorkspaceSkillSelection
17
17
  } from '@vibe-forge/types'
18
- import { normalizePath } from '@vibe-forge/utils'
18
+ import { matchesDeclaredSkillSelector, normalizePath } from '@vibe-forge/utils'
19
19
  import { mergePluginConfigs, normalizePluginConfig } from '@vibe-forge/utils/plugin-resolver'
20
20
  import { glob } from 'fast-glob'
21
21
 
@@ -26,6 +26,7 @@ import {
26
26
  isRemoteRuleReference,
27
27
  parseScopedReference
28
28
  } from '@vibe-forge/definition-core'
29
+ import { resolveConfiguredProjectSkillInstalls } from './configured-skills'
29
30
  import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
30
31
  import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
31
32
 
@@ -516,6 +517,38 @@ export const resolveExcludedSkillRefs = (selection: string[] | SkillSelection |
516
517
  return selection.type === 'exclude' ? selection.list : undefined
517
518
  }
518
519
 
520
+ const createMissingConfiguredSkillMessage = (
521
+ bundle: WorkspaceAssetBundle,
522
+ refs: string[] | undefined
523
+ ) => {
524
+ const configs = bundle.configs ?? [undefined, undefined]
525
+ const declared = resolveConfiguredProjectSkillInstalls(configs)
526
+
527
+ for (const ref of refs ?? []) {
528
+ const match = declared.find(skill => matchesDeclaredSkillSelector(ref, skill))
529
+ if (match == null) continue
530
+
531
+ return `Skill ${ref} is declared in config but is not installed. ` +
532
+ `Run \`vf skills install ${match.targetName}\` or \`vf skills install\` before selecting it.`
533
+ }
534
+
535
+ return undefined
536
+ }
537
+
538
+ export const resolveNamedSkillAssets = (
539
+ bundle: WorkspaceAssetBundle,
540
+ refs: string[] | undefined,
541
+ currentInstancePath?: string
542
+ ) => {
543
+ try {
544
+ return resolveNamedAssets(bundle.skills, refs, currentInstancePath)
545
+ } catch (error) {
546
+ const message = createMissingConfiguredSkillMessage(bundle, refs)
547
+ if (message != null) throw new Error(message)
548
+ throw error
549
+ }
550
+ }
551
+
519
552
  export const resolveSelectedSkillAssets = (
520
553
  assets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
521
554
  selection?: WorkspaceSkillSelection
@@ -540,10 +573,10 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
540
573
  ): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
541
574
  const rootAssets = bundle.skills.filter(asset => !isPluginSkillDependencyAsset(asset))
542
575
  const included = selection?.include != null && selection.include.length > 0
543
- ? resolveNamedAssets(bundle.skills, selection.include)
576
+ ? resolveNamedSkillAssets(bundle, selection.include)
544
577
  : rootAssets
545
578
  const excluded = new Set(
546
- resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
579
+ resolveNamedSkillAssets(bundle, selection?.exclude).map(asset => asset.id)
547
580
  )
548
581
  return await expandSkillAssetDependenciesWithRemoteResolution({
549
582
  allAssets: bundle.assets,