@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.
@@ -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,8 @@ import {
26
26
  isRemoteRuleReference,
27
27
  parseScopedReference
28
28
  } from '@vibe-forge/definition-core'
29
+ import { resolveConfiguredProjectSkillInstalls } from './configured-skills'
30
+ import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
29
31
  import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
30
32
 
31
33
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
@@ -359,6 +361,10 @@ const mergeEntityDefinitions = (
359
361
 
360
362
  const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
361
363
 
364
+ const isPluginSkillDependencyAsset = (asset: Extract<WorkspaceAsset, { kind: 'skill' }>) => (
365
+ asset.resolvedBy === PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
366
+ )
367
+
362
368
  const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
363
369
  [...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
364
370
  .map(item => item.displayName)
@@ -511,15 +517,48 @@ export const resolveExcludedSkillRefs = (selection: string[] | SkillSelection |
511
517
  return selection.type === 'exclude' ? selection.list : undefined
512
518
  }
513
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
+
514
552
  export const resolveSelectedSkillAssets = (
515
553
  assets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
516
554
  selection?: WorkspaceSkillSelection
517
555
  ): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
518
556
  if (selection == null) return assets
519
557
 
558
+ const rootAssets = assets.filter(asset => !isPluginSkillDependencyAsset(asset))
520
559
  const included = selection.include != null && selection.include.length > 0
521
560
  ? resolveNamedAssets(assets, selection.include)
522
- : assets
561
+ : rootAssets
523
562
  const excluded = new Set(
524
563
  resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
525
564
  )
@@ -532,11 +571,12 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
532
571
  bundle: WorkspaceAssetBundle,
533
572
  selection?: WorkspaceSkillSelection
534
573
  ): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
574
+ const rootAssets = bundle.skills.filter(asset => !isPluginSkillDependencyAsset(asset))
535
575
  const included = selection?.include != null && selection.include.length > 0
536
- ? resolveNamedAssets(bundle.skills, selection.include)
537
- : bundle.skills
576
+ ? resolveNamedSkillAssets(bundle, selection.include)
577
+ : rootAssets
538
578
  const excluded = new Set(
539
- resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
579
+ resolveNamedSkillAssets(bundle, selection?.exclude).map(asset => asset.id)
540
580
  )
541
581
  return await expandSkillAssetDependenciesWithRemoteResolution({
542
582
  allAssets: bundle.assets,
@@ -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
- }