@vibe-forge/workspace-assets 2.0.2 → 2.0.3

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.
@@ -0,0 +1,80 @@
1
+ import { join } from 'node:path'
2
+
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
6
+ import { normalizeSkillDependency } from '#~/skill-dependencies.js'
7
+
8
+ import { createWorkspace, writeDocument } from './test-helpers'
9
+
10
+ afterEach(() => {
11
+ vi.unstubAllGlobals()
12
+ })
13
+
14
+ describe('skill dependency normalization', () => {
15
+ it('parses legacy non-structured registry strings', () => {
16
+ expect(
17
+ normalizeSkillDependency('https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3')
18
+ ).toEqual({
19
+ ref: 'https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3',
20
+ name: 'lynx-devtool',
21
+ source: 'lynx/skills',
22
+ registry: 'https://skills.byted.org',
23
+ version: '1.0.3',
24
+ packageRegistry: 'https://bnpm.byted.org'
25
+ })
26
+ })
27
+
28
+ it('installs legacy non-structured registry dependencies through the skill registry API', async () => {
29
+ const workspace = await createWorkspace()
30
+ const fetchMock = vi.fn(async (url: string) => {
31
+ if (url === 'https://skills.byted.org/api/download/lynx/skills/lynx-devtool?version=1.0.3') {
32
+ return new Response(JSON.stringify({
33
+ files: [{
34
+ path: 'SKILL.md',
35
+ contents: '---\nname: lynx-devtool\ndescription: Lynx device debugging\n---\nUse devtool.\n'
36
+ }]
37
+ }))
38
+ }
39
+ return new Response('not found', { status: 404 })
40
+ })
41
+ vi.stubGlobal('fetch', fetchMock)
42
+
43
+ await writeDocument(
44
+ join(workspace, '.ai/skills/debug-lynx/SKILL.md'),
45
+ [
46
+ '---',
47
+ 'name: debug-lynx',
48
+ 'description: Debug Lynx on device',
49
+ 'dependencies:',
50
+ ' - https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3',
51
+ '---',
52
+ 'Run the full debugging workflow.'
53
+ ].join('\n')
54
+ )
55
+
56
+ const bundle = await resolveWorkspaceAssetBundle({
57
+ cwd: workspace,
58
+ configs: [undefined, undefined],
59
+ useDefaultVibeForgeMcpServer: false
60
+ })
61
+
62
+ await buildAdapterAssetPlan({
63
+ adapter: 'opencode',
64
+ bundle,
65
+ options: {
66
+ skills: {
67
+ include: ['debug-lynx']
68
+ }
69
+ }
70
+ })
71
+
72
+ expect(fetchMock).toHaveBeenCalledWith(
73
+ 'https://skills.byted.org/api/download/lynx/skills/lynx-devtool?version=1.0.3',
74
+ expect.any(Object)
75
+ )
76
+ expect(bundle.skills.find(asset => asset.name === 'lynx-devtool')?.sourcePath).toContain(
77
+ '/.ai/caches/skill-dependencies/skills.byted.org/lynx/skills/lynx-devtool/1.0.3/'
78
+ )
79
+ })
80
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -30,9 +30,9 @@
30
30
  "fast-glob": "^3.3.3",
31
31
  "front-matter": "^4.0.2",
32
32
  "js-yaml": "^4.1.1",
33
- "@vibe-forge/definition-core": "^2.0.0",
34
33
  "@vibe-forge/types": "^2.0.2",
35
- "@vibe-forge/utils": "^2.0.3"
34
+ "@vibe-forge/utils": "^2.0.3",
35
+ "@vibe-forge/definition-core": "^2.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -17,6 +17,8 @@ export interface NormalizedSkillDependency {
17
17
  name: string
18
18
  source?: string
19
19
  registry?: string
20
+ version?: string
21
+ packageRegistry?: string
20
22
  }
21
23
 
22
24
  interface DependencyExpansionParams {
@@ -42,6 +44,29 @@ const toSkillSlug = (value: string) => (
42
44
  .replace(/^-|-$/g, '')
43
45
  )
44
46
 
47
+ const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, '')
48
+
49
+ const normalizeLegacyUrl = (value: string) => {
50
+ const trimmed = value.trim()
51
+ if (trimmed === '') return undefined
52
+ const candidate = /^[a-z][a-z\d+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`
53
+ try {
54
+ return new URL(candidate)
55
+ } catch {
56
+ return undefined
57
+ }
58
+ }
59
+
60
+ const buildDependencyRef = (params: {
61
+ name: string
62
+ source?: string
63
+ version?: string
64
+ }) => (
65
+ params.source == null
66
+ ? (params.version == null ? params.name : `${params.name}@${params.version}`)
67
+ : `${params.source}@${params.name}${params.version == null ? '' : `@${params.version}`}`
68
+ )
69
+
45
70
  const resolveUniqueSkillByName = (assets: SkillAsset[], name: string) => {
46
71
  const nameSlug = toSkillSlug(name)
47
72
  const matches = assets.filter(asset => asset.name === name || toSkillSlug(asset.name) === nameSlug)
@@ -138,8 +163,38 @@ const createRegistrySkillAsset = (params: {
138
163
  } satisfies SkillAsset
139
164
  }
140
165
 
166
+ const parseLegacyRegistryDependency = (value: string): NormalizedSkillDependency | undefined => {
167
+ const ref = value.trim()
168
+ const segments = ref.split('@').map(segment => segment.trim())
169
+ if (segments.length < 3) return undefined
170
+
171
+ const hasVersion = segments.length >= 4
172
+ const nameIndex = hasVersion ? segments.length - 2 : segments.length - 1
173
+ const packageRegistry = normalizeLegacyUrl(segments[0] ?? '')
174
+ const sourceUrl = normalizeLegacyUrl(segments.slice(1, nameIndex).join('@'))
175
+ const name = asNonEmptyString(segments[nameIndex])
176
+ const version = hasVersion ? asNonEmptyString(segments[segments.length - 1]) : undefined
177
+
178
+ if (packageRegistry == null || sourceUrl == null || name == null) return undefined
179
+
180
+ const source = trimSlashes(sourceUrl.pathname)
181
+ if (source === '') return undefined
182
+
183
+ return {
184
+ ref,
185
+ name,
186
+ source,
187
+ registry: sourceUrl.origin,
188
+ ...(version == null ? {} : { version }),
189
+ packageRegistry: packageRegistry.toString().replace(/\/+$/, '')
190
+ }
191
+ }
192
+
141
193
  const parseStringDependency = (value: string): NormalizedSkillDependency => {
142
194
  const ref = value.trim()
195
+ const legacyRegistryDependency = parseLegacyRegistryDependency(ref)
196
+ if (legacyRegistryDependency != null) return legacyRegistryDependency
197
+
143
198
  const atIndex = ref.lastIndexOf('@')
144
199
  if (atIndex > 0 && atIndex < ref.length - 1) {
145
200
  return {
@@ -174,11 +229,17 @@ export const normalizeSkillDependency = (value: unknown): NormalizedSkillDepende
174
229
  if (name == null) return undefined
175
230
 
176
231
  const source = asNonEmptyString(record.source)
232
+ const version = asNonEmptyString(record.version)
177
233
  return {
178
- ref: source == null ? name : `${source}@${name}`,
234
+ ref: buildDependencyRef({
235
+ name,
236
+ ...(source == null ? {} : { source }),
237
+ ...(version == null ? {} : { version })
238
+ }),
179
239
  name,
180
240
  ...(source == null ? {} : { source }),
181
- ...(asNonEmptyString(record.registry) == null ? {} : { registry: asNonEmptyString(record.registry) })
241
+ ...(asNonEmptyString(record.registry) == null ? {} : { registry: asNonEmptyString(record.registry) }),
242
+ ...(version == null ? {} : { version })
182
243
  }
183
244
  }
184
245
 
@@ -60,6 +60,13 @@ const toCacheSegment = (value: string) => (
60
60
  .replace(/^-+|-+$/g, '') || 'registry'
61
61
  )
62
62
 
63
+ const appendVersionQuery = (url: string, version?: string) => {
64
+ if (version == null || version.trim() === '') return url
65
+ const parsed = new URL(url)
66
+ parsed.searchParams.set('version', version.trim())
67
+ return parsed.toString()
68
+ }
69
+
63
70
  const resolveConfiguredRegistry = (
64
71
  projectConfig: Config | undefined,
65
72
  userConfig: Config | undefined
@@ -146,7 +153,10 @@ const resolveRegistrySkillTarget = async (
146
153
  }
147
154
  }
148
155
 
149
- const searchUrl = `${registry.searchBaseUrl}/api/search?q=${encodeURIComponent(dependency.name)}&limit=10`
156
+ const searchUrl = appendVersionQuery(
157
+ `${registry.searchBaseUrl}/api/search?q=${encodeURIComponent(dependency.name)}&limit=10`,
158
+ dependency.version
159
+ )
150
160
  const searchResult = await fetchJson<{
151
161
  skills?: RegistrySearchSkill[]
152
162
  }>(searchUrl)
@@ -230,6 +240,7 @@ const buildInstallDir = (params: {
230
240
  registry: RegistryOptions
231
241
  source: string
232
242
  slug: string
243
+ version?: string
233
244
  }) => {
234
245
  let registryKey = params.registry.downloadBaseUrl
235
246
  try {
@@ -244,7 +255,8 @@ const buildInstallDir = (params: {
244
255
  'skill-dependencies',
245
256
  toCacheSegment(registryKey),
246
257
  ...params.source.split('/').map(toCacheSegment),
247
- toCacheSegment(params.slug)
258
+ toCacheSegment(params.slug),
259
+ ...(params.version == null ? [] : [toCacheSegment(params.version)])
248
260
  )
249
261
  }
250
262
 
@@ -267,14 +279,18 @@ export const installRegistrySkillDependency = async (params: {
267
279
  throw new Error(`Skill dependency source ${target.source} must use owner/repo format`)
268
280
  }
269
281
 
270
- const downloadUrl = `${registry.downloadBaseUrl}/api/download/${encodeURIComponent(owner)}/${
271
- encodeURIComponent(repo)
272
- }/${encodeURIComponent(target.slug)}`
282
+ const downloadUrl = appendVersionQuery(
283
+ `${registry.downloadBaseUrl}/api/download/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${
284
+ encodeURIComponent(target.slug)
285
+ }`,
286
+ params.dependency.version
287
+ )
273
288
  const installDir = buildInstallDir({
274
289
  cwd: params.cwd,
275
290
  registry,
276
291
  source: target.source,
277
- slug: target.slug
292
+ slug: target.slug,
293
+ version: params.dependency.version
278
294
  })
279
295
  const skillPath = resolve(installDir, 'SKILL.md')
280
296