@vibe-forge/workspace-assets 2.0.1 → 2.0.2

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,175 +0,0 @@
1
- /* eslint-disable import/first -- hoisted vitest mocks must be declared before importing the bundle entrypoint */
2
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
3
- import os from 'node:os'
4
- import path, { join } from 'node:path'
5
-
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
-
8
- const mocks = vi.hoisted(() => ({
9
- findSkillsCli: vi.fn(),
10
- installSkillsCliRefToTemp: vi.fn(),
11
- installSkillsCliSkillToTemp: vi.fn()
12
- }))
13
-
14
- vi.mock('@vibe-forge/utils/skills-cli', async () => {
15
- const actual = await vi.importActual<typeof import('@vibe-forge/utils/skills-cli')>('@vibe-forge/utils/skills-cli')
16
- return {
17
- ...actual,
18
- findSkillsCli: mocks.findSkillsCli,
19
- installSkillsCliRefToTemp: mocks.installSkillsCliRefToTemp,
20
- installSkillsCliSkillToTemp: mocks.installSkillsCliSkillToTemp
21
- }
22
- })
23
-
24
- import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
25
-
26
- import { createWorkspace, writeDocument } from './test-helpers'
27
-
28
- describe('skills CLI dependency resolution', () => {
29
- let installWorkspace: string
30
-
31
- beforeEach(async () => {
32
- installWorkspace = await mkdtemp(path.join(os.tmpdir(), 'vf-skills-cli-dependency-'))
33
- vi.clearAllMocks()
34
- })
35
-
36
- afterEach(async () => {
37
- await rm(installWorkspace, { recursive: true, force: true })
38
- })
39
-
40
- it('installs missing bare-name dependencies through skills CLI by default', async () => {
41
- const workspace = await createWorkspace()
42
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
43
- await mkdir(installedSkillDir, { recursive: true })
44
- await writeFile(
45
- join(installedSkillDir, 'SKILL.md'),
46
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
47
- )
48
-
49
- mocks.findSkillsCli.mockResolvedValue([
50
- {
51
- installRef: 'anthropics/skills@frontend-design',
52
- source: 'anthropics/skills',
53
- skill: 'frontend-design'
54
- }
55
- ])
56
- mocks.installSkillsCliRefToTemp.mockResolvedValue({
57
- tempDir: installWorkspace,
58
- installedSkill: {
59
- dirName: 'frontend-design',
60
- name: 'frontend-design',
61
- sourcePath: installedSkillDir
62
- }
63
- })
64
-
65
- await writeDocument(
66
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
67
- [
68
- '---',
69
- 'name: app-builder',
70
- 'description: Build apps',
71
- 'dependencies:',
72
- ' - frontend-design',
73
- '---',
74
- 'Build the app.'
75
- ].join('\n')
76
- )
77
-
78
- const bundle = await resolveWorkspaceAssetBundle({
79
- cwd: workspace,
80
- configs: [undefined, undefined],
81
- useDefaultVibeForgeMcpServer: false
82
- })
83
-
84
- await buildAdapterAssetPlan({
85
- adapter: 'opencode',
86
- bundle,
87
- options: {
88
- skills: {
89
- include: ['app-builder']
90
- }
91
- }
92
- })
93
-
94
- const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
95
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
96
- expect(dependency?.sourcePath).toContain(
97
- '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
98
- )
99
- expect(mocks.findSkillsCli).toHaveBeenCalledWith({
100
- config: undefined,
101
- query: 'frontend-design'
102
- })
103
- expect(mocks.installSkillsCliRefToTemp).toHaveBeenCalledWith({
104
- config: undefined,
105
- installRef: 'anthropics/skills@frontend-design'
106
- })
107
- })
108
-
109
- it('merges top-level skillsCli config ahead of legacy skills.cli aliases', async () => {
110
- const workspace = await createWorkspace()
111
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
112
- await mkdir(installedSkillDir, { recursive: true })
113
- await writeFile(
114
- join(installedSkillDir, 'SKILL.md'),
115
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design system.\n'
116
- )
117
-
118
- mocks.installSkillsCliSkillToTemp.mockResolvedValue({
119
- tempDir: installWorkspace,
120
- installedSkill: {
121
- dirName: 'frontend-design',
122
- name: 'frontend-design',
123
- sourcePath: installedSkillDir
124
- }
125
- })
126
-
127
- await writeDocument(
128
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
129
- [
130
- '---',
131
- 'name: app-builder',
132
- 'description: Build apps',
133
- 'dependencies:',
134
- ' - example-source/default/public@frontend-design',
135
- '---',
136
- 'Build the app.'
137
- ].join('\n')
138
- )
139
-
140
- const bundle = await resolveWorkspaceAssetBundle({
141
- cwd: workspace,
142
- configs: [{
143
- skills: {
144
- cli: {
145
- package: 'legacy-skills'
146
- }
147
- },
148
- skillsCli: {
149
- package: '@byted/skills',
150
- registry: 'https://registry.example.com'
151
- }
152
- }, undefined],
153
- useDefaultVibeForgeMcpServer: false
154
- })
155
-
156
- await buildAdapterAssetPlan({
157
- adapter: 'opencode',
158
- bundle,
159
- options: {
160
- skills: {
161
- include: ['app-builder']
162
- }
163
- }
164
- })
165
-
166
- expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
167
- config: {
168
- package: '@byted/skills',
169
- registry: 'https://registry.example.com'
170
- },
171
- skill: 'frontend-design',
172
- source: 'example-source/default/public'
173
- })
174
- })
175
- })
@@ -1,99 +0,0 @@
1
- import { access } from 'node:fs/promises'
2
- import process from 'node:process'
3
-
4
- import type { Config, ConfiguredSkillInstallConfig, SkillsCliConfig } from '@vibe-forge/types'
5
- import {
6
- installProjectSkill,
7
- normalizeProjectSkillInstall,
8
- resolveConfiguredSkillInstalls as resolveDeclaredConfiguredSkillInstalls,
9
- resolveProjectAiPath,
10
- resolveSkillsCliRuntimeConfig
11
- } from '@vibe-forge/utils'
12
- import type { NormalizedProjectSkillInstall } from '@vibe-forge/utils'
13
-
14
- const resolveConfiguredSkillsCliConfig = (configs: [Config?, Config?]) => {
15
- const [projectConfig, userConfig] = configs
16
- const merged = {
17
- ...(resolveSkillsCliRuntimeConfig(projectConfig) ?? {}),
18
- ...(resolveSkillsCliRuntimeConfig(userConfig) ?? {})
19
- } satisfies SkillsCliConfig
20
-
21
- return Object.keys(merged).length === 0 ? undefined : merged
22
- }
23
-
24
- const resolveConfiguredSkillInstalls = (configs: [Config?, Config?]) => (
25
- [
26
- ...resolveDeclaredConfiguredSkillInstalls(configs[0]?.skills),
27
- ...resolveDeclaredConfiguredSkillInstalls(configs[1]?.skills)
28
- ]
29
- .map((item) => normalizeProjectSkillInstall(item as string | ConfiguredSkillInstallConfig))
30
- .filter((item): item is NormalizedProjectSkillInstall => item != null)
31
- )
32
-
33
- const pathExists = async (targetPath: string) => {
34
- try {
35
- await access(targetPath)
36
- return true
37
- } catch {
38
- return false
39
- }
40
- }
41
-
42
- const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
43
- const seen = new Map<string, string>()
44
-
45
- for (const skill of skills) {
46
- const previous = seen.get(skill.targetDirName)
47
- if (previous != null) {
48
- throw new Error(
49
- `Configured skills "${previous}" and "${skill.ref}" resolve to the same target "${skill.targetDirName}"`
50
- )
51
- }
52
- seen.set(skill.targetDirName, skill.ref)
53
- }
54
- }
55
-
56
- export const ensureConfiguredProjectSkills = async (params: {
57
- configs: [Config?, Config?]
58
- updateInstalledSkills?: boolean
59
- workspaceFolder: string
60
- }) => {
61
- const installs = resolveConfiguredSkillInstalls(params.configs)
62
- if (installs.length === 0) {
63
- return []
64
- }
65
-
66
- ensureUniqueTargets(installs)
67
-
68
- const skillsCliConfig = resolveConfiguredSkillsCliConfig(params.configs)
69
- const ensured: Array<{ dirName: string; skillPath: string }> = []
70
-
71
- for (const skill of installs) {
72
- const skillPath = resolveProjectAiPath(
73
- params.workspaceFolder,
74
- process.env,
75
- 'skills',
76
- skill.targetDirName,
77
- 'SKILL.md'
78
- )
79
- if (params.updateInstalledSkills !== true && await pathExists(skillPath)) {
80
- ensured.push({
81
- dirName: skill.targetDirName,
82
- skillPath
83
- })
84
- continue
85
- }
86
-
87
- ensured.push(
88
- await installProjectSkill({
89
- config: skillsCliConfig,
90
- force: true,
91
- registry: undefined,
92
- skill,
93
- workspaceFolder: params.workspaceFolder
94
- })
95
- )
96
- }
97
-
98
- return ensured
99
- }
@@ -1,208 +0,0 @@
1
- import { access, copyFile, lstat, mkdir, readdir, rename, rm } from 'node:fs/promises'
2
- import { dirname, resolve } from 'node:path'
3
- import process from 'node:process'
4
- import { setTimeout as delay } from 'node:timers/promises'
5
-
6
- import type { Config, SkillsCliConfig } from '@vibe-forge/types'
7
- import { resolveSkillsCliRuntimeConfig } from '@vibe-forge/utils'
8
- import { resolveProjectSharedCachePath } from '@vibe-forge/utils/project-cache-path'
9
- import {
10
- findSkillsCli,
11
- installSkillsCliRefToTemp,
12
- installSkillsCliSkillToTemp,
13
- resolveSkillsCliRegistry,
14
- toSkillSlug
15
- } from '@vibe-forge/utils/skills-cli'
16
-
17
- import type { NormalizedSkillDependency } from './skill-dependencies'
18
-
19
- const INSTALL_LOCK_TIMEOUT_MS = 30_000
20
- const INSTALL_LOCK_RETRY_MS = 100
21
-
22
- const toCacheSegment = (value: string) => (
23
- value
24
- .trim()
25
- .toLowerCase()
26
- .replace(/[^a-z0-9._-]+/g, '-')
27
- .replace(/^-+|-+$/g, '') || 'default'
28
- )
29
-
30
- const pathExists = async (targetPath: string) => {
31
- try {
32
- await access(targetPath)
33
- return true
34
- } catch {
35
- return false
36
- }
37
- }
38
-
39
- const withInstallLock = async <T>(lockDir: string, callback: () => Promise<T>) => {
40
- const start = Date.now()
41
- await mkdir(dirname(lockDir), { recursive: true })
42
-
43
- while (true) {
44
- try {
45
- await mkdir(lockDir)
46
- break
47
- } catch (error) {
48
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
49
- if (Date.now() - start > INSTALL_LOCK_TIMEOUT_MS) {
50
- throw new Error(`Timed out waiting for skill dependency install lock ${lockDir}`)
51
- }
52
- await delay(INSTALL_LOCK_RETRY_MS)
53
- }
54
- }
55
-
56
- try {
57
- return await callback()
58
- } finally {
59
- await rm(lockDir, { recursive: true, force: true })
60
- }
61
- }
62
-
63
- const copyRegularFiles = async (sourceDir: string, targetDir: string) => {
64
- let fileCount = 0
65
- const entries = await readdir(sourceDir, { withFileTypes: true })
66
-
67
- await mkdir(targetDir, { recursive: true })
68
-
69
- for (const entry of entries) {
70
- const sourcePath = resolve(sourceDir, entry.name)
71
- const targetPath = resolve(targetDir, entry.name)
72
- const stat = await lstat(sourcePath)
73
-
74
- if (stat.isDirectory()) {
75
- fileCount += await copyRegularFiles(sourcePath, targetPath)
76
- continue
77
- }
78
-
79
- if (!stat.isFile()) continue
80
-
81
- await mkdir(dirname(targetPath), { recursive: true })
82
- await copyFile(sourcePath, targetPath)
83
- fileCount += 1
84
- }
85
-
86
- return fileCount
87
- }
88
-
89
- const pickSearchResult = (results: Awaited<ReturnType<typeof findSkillsCli>>, name: string) => {
90
- const slug = toSkillSlug(name)
91
- return results.find(result => (
92
- result.skill === name ||
93
- toSkillSlug(result.skill) === slug
94
- )) ?? results[0]
95
- }
96
-
97
- const resolveConfiguredSkillsCliConfig = (configs: [Config?, Config?]) => {
98
- const [projectConfig, userConfig] = configs
99
- const merged = {
100
- ...(resolveSkillsCliRuntimeConfig(projectConfig) ?? {}),
101
- ...(resolveSkillsCliRuntimeConfig(userConfig) ?? {})
102
- } satisfies SkillsCliConfig
103
-
104
- return Object.keys(merged).length === 0 ? undefined : merged
105
- }
106
-
107
- const buildInstallDir = (params: {
108
- config?: SkillsCliConfig
109
- cwd: string
110
- skill: string
111
- source: string
112
- }) => {
113
- const registry = resolveSkillsCliRegistry({
114
- config: params.config
115
- }) ?? 'default'
116
- return resolveProjectSharedCachePath(
117
- params.cwd,
118
- process.env,
119
- 'skill-dependencies',
120
- 'skills-cli',
121
- toCacheSegment(params.config?.package ?? 'skills'),
122
- toCacheSegment(params.config?.version ?? 'latest'),
123
- toCacheSegment(registry),
124
- ...params.source.split('/').map(toCacheSegment),
125
- toCacheSegment(params.skill)
126
- )
127
- }
128
-
129
- export const installSkillsCliDependency = async (params: {
130
- cwd: string
131
- configs: [Config?, Config?]
132
- dependency: NormalizedSkillDependency
133
- }) => {
134
- const config = resolveConfiguredSkillsCliConfig(params.configs)
135
- const resolvedTarget = params.dependency.source != null
136
- ? {
137
- skill: params.dependency.name,
138
- source: params.dependency.source
139
- }
140
- : await (async () => {
141
- const searchResults = await findSkillsCli({
142
- config,
143
- query: params.dependency.name
144
- })
145
- const selected = pickSearchResult(searchResults, params.dependency.name)
146
- if (selected == null) {
147
- throw new Error(`Skill ${params.dependency.name} was not found by the skills CLI search.`)
148
- }
149
-
150
- return {
151
- installRef: selected.installRef,
152
- skill: selected.skill,
153
- source: selected.source
154
- }
155
- })()
156
-
157
- const installDir = buildInstallDir({
158
- config,
159
- cwd: params.cwd,
160
- skill: resolvedTarget.skill,
161
- source: resolvedTarget.source
162
- })
163
- const skillPath = resolve(installDir, 'SKILL.md')
164
-
165
- return await withInstallLock(`${installDir}.lock`, async () => {
166
- if (await pathExists(skillPath)) {
167
- return {
168
- installDir,
169
- skillPath
170
- }
171
- }
172
-
173
- const tempInstallDir = `${installDir}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
174
- await rm(tempInstallDir, { recursive: true, force: true })
175
- await mkdir(tempInstallDir, { recursive: true })
176
-
177
- const installResult = 'installRef' in resolvedTarget
178
- ? await installSkillsCliRefToTemp({
179
- config,
180
- installRef: resolvedTarget.installRef
181
- })
182
- : await installSkillsCliSkillToTemp({
183
- config,
184
- skill: resolvedTarget.skill,
185
- source: resolvedTarget.source
186
- })
187
-
188
- try {
189
- await copyRegularFiles(installResult.installedSkill.sourcePath, tempInstallDir)
190
- if (!await pathExists(resolve(tempInstallDir, 'SKILL.md'))) {
191
- throw new Error(`Skill dependency ${params.dependency.ref} did not include SKILL.md`)
192
- }
193
-
194
- await rm(installDir, { recursive: true, force: true })
195
- await rename(tempInstallDir, installDir)
196
- } catch (error) {
197
- await rm(tempInstallDir, { recursive: true, force: true })
198
- throw error
199
- } finally {
200
- await rm(installResult.tempDir, { recursive: true, force: true })
201
- }
202
-
203
- return {
204
- installDir,
205
- skillPath
206
- }
207
- })
208
- }