@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.
|
|
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:
|
|
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
|
|
package/src/skill-registry.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
271
|
-
encodeURIComponent(repo)
|
|
272
|
-
|
|
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
|
|