@vibe-forge/workspace-assets 2.0.3 → 2.0.4-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.
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +1 -1
- package/__tests__/adapter-asset-plan.spec.ts +67 -65
- package/__tests__/bundle.spec.ts +208 -138
- package/__tests__/prompt-builders.spec.ts +2 -6
- package/__tests__/skill-dependencies-cli.spec.ts +234 -0
- package/package.json +2 -2
- package/src/bundle-internal.ts +18 -3
- package/src/bundle.ts +2 -0
- package/src/configured-skills.ts +85 -0
- package/src/prompt-selection.ts +2 -2
- package/src/selection-internal.ts +2 -2
- package/src/skill-dependencies.ts +17 -96
- package/src/skills-cli-dependency-helpers.ts +94 -0
- package/src/skills-cli-dependency.ts +104 -0
- package/src/task-tool-guidance.ts +2 -4
- package/__tests__/skill-dependencies.spec.ts +0 -80
- package/src/skill-registry.ts +0 -345
- package/vibe-forge-workspace-assets-2.0.2.tgz +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/* eslint-disable import/first -- hoisted vitest mocks must be declared before importing the bundle entrypoint */
|
|
2
|
+
import { access, mkdir, mkdtemp, rm, utimes, 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
|
+
const pathExists = async (targetPath: string) => {
|
|
32
|
+
try {
|
|
33
|
+
await access(targetPath)
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
installWorkspace = await mkdtemp(path.join(os.tmpdir(), 'vf-skills-cli-dependency-'))
|
|
42
|
+
vi.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await rm(installWorkspace, { recursive: true, force: true })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('installs missing bare-name dependencies through skills CLI by default', async () => {
|
|
50
|
+
const workspace = await createWorkspace()
|
|
51
|
+
const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
|
|
52
|
+
await mkdir(installedSkillDir, { recursive: true })
|
|
53
|
+
await writeFile(
|
|
54
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
55
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
mocks.findSkillsCli.mockResolvedValue([
|
|
59
|
+
{
|
|
60
|
+
installRef: 'anthropics/skills@frontend-design',
|
|
61
|
+
source: 'anthropics/skills',
|
|
62
|
+
skill: 'frontend-design'
|
|
63
|
+
}
|
|
64
|
+
])
|
|
65
|
+
mocks.installSkillsCliRefToTemp.mockResolvedValue({
|
|
66
|
+
tempDir: installWorkspace,
|
|
67
|
+
installedSkill: {
|
|
68
|
+
dirName: 'frontend-design',
|
|
69
|
+
name: 'frontend-design',
|
|
70
|
+
sourcePath: installedSkillDir
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await writeDocument(
|
|
75
|
+
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
76
|
+
[
|
|
77
|
+
'---',
|
|
78
|
+
'name: app-builder',
|
|
79
|
+
'description: Build apps',
|
|
80
|
+
'dependencies:',
|
|
81
|
+
' - frontend-design',
|
|
82
|
+
'---',
|
|
83
|
+
'Build the app.'
|
|
84
|
+
].join('\n')
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
88
|
+
cwd: workspace,
|
|
89
|
+
configs: [undefined, undefined],
|
|
90
|
+
useDefaultVibeForgeMcpServer: false
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await buildAdapterAssetPlan({
|
|
94
|
+
adapter: 'opencode',
|
|
95
|
+
bundle,
|
|
96
|
+
options: {
|
|
97
|
+
skills: {
|
|
98
|
+
include: ['app-builder']
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
|
|
104
|
+
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
|
|
105
|
+
expect(dependency?.sourcePath).toContain(
|
|
106
|
+
'/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/'
|
|
107
|
+
)
|
|
108
|
+
expect(mocks.findSkillsCli).toHaveBeenCalledWith({
|
|
109
|
+
query: 'frontend-design'
|
|
110
|
+
})
|
|
111
|
+
expect(mocks.installSkillsCliRefToTemp).toHaveBeenCalledWith({
|
|
112
|
+
installRef: 'anthropics/skills@frontend-design'
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('parses registry and version from dependency specs', async () => {
|
|
117
|
+
const workspace = await createWorkspace()
|
|
118
|
+
const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
|
|
119
|
+
await mkdir(installedSkillDir, { recursive: true })
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
122
|
+
'---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design system.\n'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
mocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
126
|
+
tempDir: installWorkspace,
|
|
127
|
+
installedSkill: {
|
|
128
|
+
dirName: 'frontend-design',
|
|
129
|
+
name: 'frontend-design',
|
|
130
|
+
sourcePath: installedSkillDir
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await writeDocument(
|
|
135
|
+
join(workspace, '.ai/skills/app-builder/SKILL.md'),
|
|
136
|
+
[
|
|
137
|
+
'---',
|
|
138
|
+
'name: app-builder',
|
|
139
|
+
'description: Build apps',
|
|
140
|
+
'dependencies:',
|
|
141
|
+
' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
|
|
142
|
+
'---',
|
|
143
|
+
'Build the app.'
|
|
144
|
+
].join('\n')
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
148
|
+
cwd: workspace,
|
|
149
|
+
configs: [undefined, undefined],
|
|
150
|
+
useDefaultVibeForgeMcpServer: false
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await buildAdapterAssetPlan({
|
|
154
|
+
adapter: 'opencode',
|
|
155
|
+
bundle,
|
|
156
|
+
options: {
|
|
157
|
+
skills: {
|
|
158
|
+
include: ['app-builder']
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
164
|
+
registry: 'https://registry.example.com',
|
|
165
|
+
skill: 'frontend-design',
|
|
166
|
+
source: 'example-source/default/public',
|
|
167
|
+
version: '1.0.3'
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('clears stale dependency install locks before retrying a direct startup install', async () => {
|
|
172
|
+
const workspace = await createWorkspace()
|
|
173
|
+
const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'lynx-cat')
|
|
174
|
+
await mkdir(installedSkillDir, { recursive: true })
|
|
175
|
+
await writeFile(
|
|
176
|
+
join(installedSkillDir, 'SKILL.md'),
|
|
177
|
+
'---\nname: lynx-cat\ndescription: Lynx helper\n---\nDebug Lynx apps.\n'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
mocks.installSkillsCliSkillToTemp.mockResolvedValue({
|
|
181
|
+
tempDir: installWorkspace,
|
|
182
|
+
installedSkill: {
|
|
183
|
+
dirName: 'lynx-cat',
|
|
184
|
+
name: 'lynx-cat',
|
|
185
|
+
sourcePath: installedSkillDir
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
await writeDocument(
|
|
190
|
+
join(workspace, '.ai/skills/lynx-miniapp/SKILL.md'),
|
|
191
|
+
[
|
|
192
|
+
'---',
|
|
193
|
+
'name: lynx-miniapp',
|
|
194
|
+
'description: lynx 调试使用',
|
|
195
|
+
'dependencies:',
|
|
196
|
+
' - https://registry.example.com@example-source/lynx/skills@lynx-cat@latest',
|
|
197
|
+
'---',
|
|
198
|
+
'这是一个测试的 lynx 调试技能'
|
|
199
|
+
].join('\n')
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const lockDir = join(
|
|
203
|
+
workspace,
|
|
204
|
+
'.ai/caches/skill-dependencies/skills-cli/skills/latest/https-registry.example.com/example-source/lynx/skills/latest/lynx-cat.lock'
|
|
205
|
+
)
|
|
206
|
+
await mkdir(lockDir, { recursive: true })
|
|
207
|
+
await utimes(lockDir, new Date(Date.now() - 120_000), new Date(Date.now() - 120_000))
|
|
208
|
+
|
|
209
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
210
|
+
cwd: workspace,
|
|
211
|
+
configs: [undefined, undefined],
|
|
212
|
+
useDefaultVibeForgeMcpServer: false
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await buildAdapterAssetPlan({
|
|
216
|
+
adapter: 'opencode',
|
|
217
|
+
bundle,
|
|
218
|
+
options: {
|
|
219
|
+
skills: {
|
|
220
|
+
include: ['lynx-miniapp']
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
|
|
226
|
+
registry: 'https://registry.example.com',
|
|
227
|
+
skill: 'lynx-cat',
|
|
228
|
+
source: 'example-source/lynx/skills',
|
|
229
|
+
version: 'latest'
|
|
230
|
+
})
|
|
231
|
+
await expect(pathExists(lockDir)).resolves.toBe(false)
|
|
232
|
+
expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['lynx-cat', 'lynx-miniapp'])
|
|
233
|
+
})
|
|
234
|
+
})
|
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.4-alpha.1",
|
|
4
4
|
"description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
|
|
5
5
|
"imports": {
|
|
6
6
|
"#~/*.js": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"front-matter": "^4.0.2",
|
|
32
32
|
"js-yaml": "^4.1.1",
|
|
33
33
|
"@vibe-forge/types": "^2.0.2",
|
|
34
|
-
"@vibe-forge/utils": "^2.0.
|
|
34
|
+
"@vibe-forge/utils": "^2.0.4-alpha.0",
|
|
35
35
|
"@vibe-forge/definition-core": "^2.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
package/src/bundle-internal.ts
CHANGED
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
resolveDefaultVibeForgeMcpServerConfig
|
|
10
10
|
} from '@vibe-forge/config'
|
|
11
11
|
import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isLegacySkillsConfig,
|
|
14
|
+
resolveProjectAiBaseDir,
|
|
15
|
+
resolveProjectAiEntitiesDir,
|
|
16
|
+
resolveRelativePath
|
|
17
|
+
} from '@vibe-forge/utils'
|
|
13
18
|
import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
|
|
14
19
|
import {
|
|
15
20
|
flattenPluginInstances,
|
|
@@ -28,6 +33,7 @@ import {
|
|
|
28
33
|
resolveSkillIdentifier,
|
|
29
34
|
resolveSpecIdentifier
|
|
30
35
|
} from '@vibe-forge/definition-core'
|
|
36
|
+
import { ensureConfiguredProjectSkills } from './configured-skills'
|
|
31
37
|
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
32
38
|
import { resolveConfiguredWorkspaceAssets } from './workspaces'
|
|
33
39
|
|
|
@@ -153,8 +159,8 @@ const warnInvalidHomeSkillRoot = (root: string) => {
|
|
|
153
159
|
|
|
154
160
|
const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
|
|
155
161
|
const [config, userConfig] = configs
|
|
156
|
-
const projectHomeBridge = config?.skills
|
|
157
|
-
const userHomeBridge = userConfig?.skills
|
|
162
|
+
const projectHomeBridge = isLegacySkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
|
|
163
|
+
const userHomeBridge = isLegacySkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
|
|
158
164
|
|
|
159
165
|
return {
|
|
160
166
|
enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
|
|
@@ -526,6 +532,8 @@ export async function collectWorkspaceAssets(params: {
|
|
|
526
532
|
plugins?: PluginConfig
|
|
527
533
|
overlaySource?: string
|
|
528
534
|
includeManagedPlugins?: boolean
|
|
535
|
+
syncConfiguredSkills?: boolean
|
|
536
|
+
updateConfiguredSkills?: boolean
|
|
529
537
|
useDefaultVibeForgeMcpServer?: boolean
|
|
530
538
|
}): Promise<{
|
|
531
539
|
assets: WorkspaceAsset[]
|
|
@@ -544,6 +552,13 @@ export async function collectWorkspaceAssets(params: {
|
|
|
544
552
|
workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
|
|
545
553
|
}> {
|
|
546
554
|
const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
|
|
555
|
+
if (params.syncConfiguredSkills === true) {
|
|
556
|
+
await ensureConfiguredProjectSkills({
|
|
557
|
+
configs: [config, userConfig],
|
|
558
|
+
updateInstalledSkills: params.updateConfiguredSkills,
|
|
559
|
+
workspaceFolder: params.cwd
|
|
560
|
+
})
|
|
561
|
+
}
|
|
547
562
|
const managedPluginConfigs = params.includeManagedPlugins === false
|
|
548
563
|
? undefined
|
|
549
564
|
: toManagedPluginConfig(await listManagedPluginInstalls(params.cwd))
|
package/src/bundle.ts
CHANGED
|
@@ -8,6 +8,8 @@ export async function resolveWorkspaceAssetBundle(params: {
|
|
|
8
8
|
plugins?: PluginConfig
|
|
9
9
|
overlaySource?: string
|
|
10
10
|
includeManagedPlugins?: boolean
|
|
11
|
+
syncConfiguredSkills?: boolean
|
|
12
|
+
updateConfiguredSkills?: boolean
|
|
11
13
|
useDefaultVibeForgeMcpServer?: boolean
|
|
12
14
|
}): Promise<WorkspaceAssetBundle> {
|
|
13
15
|
const collected = await collectWorkspaceAssets(params)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import type { Config, ConfiguredSkillInstallConfig } from '@vibe-forge/types'
|
|
5
|
+
import {
|
|
6
|
+
installProjectSkill,
|
|
7
|
+
normalizeProjectSkillInstall,
|
|
8
|
+
resolveConfiguredSkillInstalls as resolveDeclaredConfiguredSkillInstalls,
|
|
9
|
+
resolveProjectAiPath
|
|
10
|
+
} from '@vibe-forge/utils'
|
|
11
|
+
import type { NormalizedProjectSkillInstall } from '@vibe-forge/utils'
|
|
12
|
+
|
|
13
|
+
const resolveConfiguredSkillInstalls = (configs: [Config?, Config?]) => (
|
|
14
|
+
[
|
|
15
|
+
...resolveDeclaredConfiguredSkillInstalls(configs[0]?.skills),
|
|
16
|
+
...resolveDeclaredConfiguredSkillInstalls(configs[1]?.skills)
|
|
17
|
+
]
|
|
18
|
+
.map((item) => normalizeProjectSkillInstall(item as string | ConfiguredSkillInstallConfig))
|
|
19
|
+
.filter((item): item is NormalizedProjectSkillInstall => item != null)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const pathExists = async (targetPath: string) => {
|
|
23
|
+
try {
|
|
24
|
+
await access(targetPath)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
|
|
32
|
+
const seen = new Map<string, string>()
|
|
33
|
+
|
|
34
|
+
for (const skill of skills) {
|
|
35
|
+
const previous = seen.get(skill.targetDirName)
|
|
36
|
+
if (previous != null) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Configured skills "${previous}" and "${skill.ref}" resolve to the same target "${skill.targetDirName}"`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
seen.set(skill.targetDirName, skill.ref)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ensureConfiguredProjectSkills = async (params: {
|
|
46
|
+
configs: [Config?, Config?]
|
|
47
|
+
updateInstalledSkills?: boolean
|
|
48
|
+
workspaceFolder: string
|
|
49
|
+
}) => {
|
|
50
|
+
const installs = resolveConfiguredSkillInstalls(params.configs)
|
|
51
|
+
if (installs.length === 0) {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ensureUniqueTargets(installs)
|
|
56
|
+
|
|
57
|
+
const ensured: Array<{ dirName: string; skillPath: string }> = []
|
|
58
|
+
|
|
59
|
+
for (const skill of installs) {
|
|
60
|
+
const skillPath = resolveProjectAiPath(
|
|
61
|
+
params.workspaceFolder,
|
|
62
|
+
process.env,
|
|
63
|
+
'skills',
|
|
64
|
+
skill.targetDirName,
|
|
65
|
+
'SKILL.md'
|
|
66
|
+
)
|
|
67
|
+
if (params.updateInstalledSkills !== true && await pathExists(skillPath)) {
|
|
68
|
+
ensured.push({
|
|
69
|
+
dirName: skill.targetDirName,
|
|
70
|
+
skillPath
|
|
71
|
+
})
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ensured.push(
|
|
76
|
+
await installProjectSkill({
|
|
77
|
+
force: true,
|
|
78
|
+
skill,
|
|
79
|
+
workspaceFolder: params.workspaceFolder
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return ensured
|
|
85
|
+
}
|
package/src/prompt-selection.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
resolveSelectedSkillAssetsWithDependencies,
|
|
36
36
|
toDocumentDefinitions
|
|
37
37
|
} from './selection-internal'
|
|
38
|
-
import {
|
|
38
|
+
import { expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
|
|
39
39
|
import { generateWorkspaceRoutePrompt } from './workspace-prompt'
|
|
40
40
|
|
|
41
41
|
export async function resolvePromptAssetSelection(params: {
|
|
@@ -164,7 +164,7 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
164
164
|
resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
const expandedTargetSkills = await
|
|
167
|
+
const expandedTargetSkills = await expandSkillAssetDependenciesWithRemoteResolution({
|
|
168
168
|
allAssets: effectiveBundle.assets,
|
|
169
169
|
configs: effectiveBundle.configs ?? [undefined, undefined],
|
|
170
170
|
cwd: effectiveBundle.cwd,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
isRemoteRuleReference,
|
|
27
27
|
parseScopedReference
|
|
28
28
|
} from '@vibe-forge/definition-core'
|
|
29
|
-
import { expandSkillAssetDependencies,
|
|
29
|
+
import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
|
|
30
30
|
|
|
31
31
|
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
32
32
|
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
@@ -538,7 +538,7 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
|
|
|
538
538
|
const excluded = new Set(
|
|
539
539
|
resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
|
|
540
540
|
)
|
|
541
|
-
return await
|
|
541
|
+
return await expandSkillAssetDependenciesWithRemoteResolution({
|
|
542
542
|
allAssets: bundle.assets,
|
|
543
543
|
configs: bundle.configs ?? [undefined, undefined],
|
|
544
544
|
cwd: bundle.cwd,
|
|
@@ -5,20 +5,19 @@ import fm from 'front-matter'
|
|
|
5
5
|
|
|
6
6
|
import { parseScopedReference, resolveSkillIdentifier } from '@vibe-forge/definition-core'
|
|
7
7
|
import type { Config, Definition, Skill, WorkspaceAsset } from '@vibe-forge/types'
|
|
8
|
-
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
8
|
+
import { formatSkillsSpec, parseSkillsSpec, resolveRelativePath } from '@vibe-forge/utils'
|
|
9
9
|
|
|
10
10
|
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
11
|
-
import {
|
|
11
|
+
import { installSkillsCliDependency } from './skills-cli-dependency'
|
|
12
12
|
|
|
13
13
|
type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
|
|
14
14
|
|
|
15
15
|
export interface NormalizedSkillDependency {
|
|
16
16
|
ref: string
|
|
17
17
|
name: string
|
|
18
|
-
source?: string
|
|
19
18
|
registry?: string
|
|
19
|
+
source?: string
|
|
20
20
|
version?: string
|
|
21
|
-
packageRegistry?: string
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
interface DependencyExpansionParams {
|
|
@@ -44,29 +43,6 @@ const toSkillSlug = (value: string) => (
|
|
|
44
43
|
.replace(/^-|-$/g, '')
|
|
45
44
|
)
|
|
46
45
|
|
|
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
|
-
|
|
70
46
|
const resolveUniqueSkillByName = (assets: SkillAsset[], name: string) => {
|
|
71
47
|
const nameSlug = toSkillSlug(name)
|
|
72
48
|
const matches = assets.filter(asset => asset.name === name || toSkillSlug(asset.name) === nameSlug)
|
|
@@ -144,7 +120,7 @@ const parseFrontmatterSkill = async (path: string): Promise<Definition<Skill>> =
|
|
|
144
120
|
}
|
|
145
121
|
}
|
|
146
122
|
|
|
147
|
-
const
|
|
123
|
+
const createResolvedSkillAsset = (params: {
|
|
148
124
|
cwd: string
|
|
149
125
|
definition: Definition<Skill>
|
|
150
126
|
}) => {
|
|
@@ -163,82 +139,28 @@ const createRegistrySkillAsset = (params: {
|
|
|
163
139
|
} satisfies SkillAsset
|
|
164
140
|
}
|
|
165
141
|
|
|
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
|
-
|
|
193
|
-
const parseStringDependency = (value: string): NormalizedSkillDependency => {
|
|
194
|
-
const ref = value.trim()
|
|
195
|
-
const legacyRegistryDependency = parseLegacyRegistryDependency(ref)
|
|
196
|
-
if (legacyRegistryDependency != null) return legacyRegistryDependency
|
|
197
|
-
|
|
198
|
-
const atIndex = ref.lastIndexOf('@')
|
|
199
|
-
if (atIndex > 0 && atIndex < ref.length - 1) {
|
|
200
|
-
return {
|
|
201
|
-
ref,
|
|
202
|
-
source: ref.slice(0, atIndex),
|
|
203
|
-
name: ref.slice(atIndex + 1)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const sourcePathMatch = ref.match(/^([^/\s]+\/[^/\s]+)\/([^/\s]+)$/)
|
|
208
|
-
if (sourcePathMatch != null) {
|
|
209
|
-
return {
|
|
210
|
-
ref,
|
|
211
|
-
source: sourcePathMatch[1],
|
|
212
|
-
name: sourcePathMatch[2]
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
ref,
|
|
218
|
-
name: ref
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
142
|
export const normalizeSkillDependency = (value: unknown): NormalizedSkillDependency | undefined => {
|
|
223
143
|
const stringValue = asNonEmptyString(value)
|
|
224
|
-
if (stringValue != null) return
|
|
144
|
+
if (stringValue != null) return parseSkillsSpec(stringValue)
|
|
225
145
|
|
|
226
146
|
if (value == null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
227
147
|
const record = value as Record<string, unknown>
|
|
228
148
|
const name = asNonEmptyString(record.name)
|
|
229
149
|
if (name == null) return undefined
|
|
230
150
|
|
|
151
|
+
const registry = asNonEmptyString(record.registry)
|
|
231
152
|
const source = asNonEmptyString(record.source)
|
|
232
153
|
const version = asNonEmptyString(record.version)
|
|
233
154
|
return {
|
|
234
|
-
ref:
|
|
155
|
+
ref: formatSkillsSpec({
|
|
235
156
|
name,
|
|
236
|
-
|
|
237
|
-
|
|
157
|
+
registry,
|
|
158
|
+
source,
|
|
159
|
+
version
|
|
238
160
|
}),
|
|
239
161
|
name,
|
|
162
|
+
...(registry == null ? {} : { registry }),
|
|
240
163
|
...(source == null ? {} : { source }),
|
|
241
|
-
...(asNonEmptyString(record.registry) == null ? {} : { registry: asNonEmptyString(record.registry) }),
|
|
242
164
|
...(version == null ? {} : { version })
|
|
243
165
|
}
|
|
244
166
|
}
|
|
@@ -303,7 +225,7 @@ export const expandSkillAssetDependencies = (
|
|
|
303
225
|
return selected
|
|
304
226
|
}
|
|
305
227
|
|
|
306
|
-
export const
|
|
228
|
+
export const expandSkillAssetDependenciesWithRemoteResolution = async (
|
|
307
229
|
params: DependencyExpansionParams
|
|
308
230
|
) => {
|
|
309
231
|
const selected: SkillAsset[] = []
|
|
@@ -321,16 +243,16 @@ export const expandSkillAssetDependenciesWithRegistry = async (
|
|
|
321
243
|
dependency: NormalizedSkillDependency,
|
|
322
244
|
currentInstancePath?: string
|
|
323
245
|
) => {
|
|
324
|
-
const fetchKey =
|
|
246
|
+
const fetchKey = dependency.ref
|
|
325
247
|
if (!fetchedDependencyRefs.has(fetchKey)) {
|
|
326
248
|
fetchedDependencyRefs.add(fetchKey)
|
|
327
|
-
const installed = await
|
|
249
|
+
const installed = await installSkillsCliDependency({
|
|
328
250
|
cwd: params.cwd,
|
|
329
251
|
configs: params.configs,
|
|
330
252
|
dependency
|
|
331
253
|
})
|
|
332
254
|
const definition = await parseFrontmatterSkill(installed.skillPath)
|
|
333
|
-
const dependencyAsset =
|
|
255
|
+
const dependencyAsset = createResolvedSkillAsset({
|
|
334
256
|
cwd: params.cwd,
|
|
335
257
|
definition
|
|
336
258
|
})
|
|
@@ -395,14 +317,13 @@ export const expandSkillAssetDependenciesWithRegistry = async (
|
|
|
395
317
|
const dependencyAsset = await installDependencyAsset(dependency, asset.instancePath).catch((error: unknown) => {
|
|
396
318
|
if (
|
|
397
319
|
localOrBridgedDependency != null &&
|
|
398
|
-
dependency.source == null
|
|
399
|
-
dependency.registry == null
|
|
320
|
+
dependency.source == null
|
|
400
321
|
) {
|
|
401
322
|
return localOrBridgedDependency
|
|
402
323
|
}
|
|
403
324
|
throw error
|
|
404
325
|
}) ?? (
|
|
405
|
-
dependency.source == null
|
|
326
|
+
dependency.source == null
|
|
406
327
|
? localOrBridgedDependency
|
|
407
328
|
: undefined
|
|
408
329
|
)
|