@vibe-forge/workspace-assets 2.0.2 → 2.0.4-alpha.0
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 +22 -40
- 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/src/skill-registry.ts +0 -329
- 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.0",
|
|
4
4
|
"description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
|
|
5
5
|
"imports": {
|
|
6
6
|
"#~/*.js": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"js-yaml": "^4.1.1",
|
|
33
33
|
"@vibe-forge/definition-core": "^2.0.0",
|
|
34
34
|
"@vibe-forge/types": "^2.0.2",
|
|
35
|
-
"@vibe-forge/utils": "^2.0.
|
|
35
|
+
"@vibe-forge/utils": "^2.0.4-alpha.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/js-yaml": "^4.0.9"
|
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,18 +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
|
+
version?: string
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface DependencyExpansionParams {
|
|
@@ -119,7 +120,7 @@ const parseFrontmatterSkill = async (path: string): Promise<Definition<Skill>> =
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const
|
|
123
|
+
const createResolvedSkillAsset = (params: {
|
|
123
124
|
cwd: string
|
|
124
125
|
definition: Definition<Skill>
|
|
125
126
|
}) => {
|
|
@@ -138,47 +139,29 @@ const createRegistrySkillAsset = (params: {
|
|
|
138
139
|
} satisfies SkillAsset
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const parseStringDependency = (value: string): NormalizedSkillDependency => {
|
|
142
|
-
const ref = value.trim()
|
|
143
|
-
const atIndex = ref.lastIndexOf('@')
|
|
144
|
-
if (atIndex > 0 && atIndex < ref.length - 1) {
|
|
145
|
-
return {
|
|
146
|
-
ref,
|
|
147
|
-
source: ref.slice(0, atIndex),
|
|
148
|
-
name: ref.slice(atIndex + 1)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const sourcePathMatch = ref.match(/^([^/\s]+\/[^/\s]+)\/([^/\s]+)$/)
|
|
153
|
-
if (sourcePathMatch != null) {
|
|
154
|
-
return {
|
|
155
|
-
ref,
|
|
156
|
-
source: sourcePathMatch[1],
|
|
157
|
-
name: sourcePathMatch[2]
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
ref,
|
|
163
|
-
name: ref
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
142
|
export const normalizeSkillDependency = (value: unknown): NormalizedSkillDependency | undefined => {
|
|
168
143
|
const stringValue = asNonEmptyString(value)
|
|
169
|
-
if (stringValue != null) return
|
|
144
|
+
if (stringValue != null) return parseSkillsSpec(stringValue)
|
|
170
145
|
|
|
171
146
|
if (value == null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
172
147
|
const record = value as Record<string, unknown>
|
|
173
148
|
const name = asNonEmptyString(record.name)
|
|
174
149
|
if (name == null) return undefined
|
|
175
150
|
|
|
151
|
+
const registry = asNonEmptyString(record.registry)
|
|
176
152
|
const source = asNonEmptyString(record.source)
|
|
153
|
+
const version = asNonEmptyString(record.version)
|
|
177
154
|
return {
|
|
178
|
-
ref:
|
|
155
|
+
ref: formatSkillsSpec({
|
|
156
|
+
name,
|
|
157
|
+
registry,
|
|
158
|
+
source,
|
|
159
|
+
version
|
|
160
|
+
}),
|
|
179
161
|
name,
|
|
162
|
+
...(registry == null ? {} : { registry }),
|
|
180
163
|
...(source == null ? {} : { source }),
|
|
181
|
-
...(
|
|
164
|
+
...(version == null ? {} : { version })
|
|
182
165
|
}
|
|
183
166
|
}
|
|
184
167
|
|
|
@@ -242,7 +225,7 @@ export const expandSkillAssetDependencies = (
|
|
|
242
225
|
return selected
|
|
243
226
|
}
|
|
244
227
|
|
|
245
|
-
export const
|
|
228
|
+
export const expandSkillAssetDependenciesWithRemoteResolution = async (
|
|
246
229
|
params: DependencyExpansionParams
|
|
247
230
|
) => {
|
|
248
231
|
const selected: SkillAsset[] = []
|
|
@@ -260,16 +243,16 @@ export const expandSkillAssetDependenciesWithRegistry = async (
|
|
|
260
243
|
dependency: NormalizedSkillDependency,
|
|
261
244
|
currentInstancePath?: string
|
|
262
245
|
) => {
|
|
263
|
-
const fetchKey =
|
|
246
|
+
const fetchKey = dependency.ref
|
|
264
247
|
if (!fetchedDependencyRefs.has(fetchKey)) {
|
|
265
248
|
fetchedDependencyRefs.add(fetchKey)
|
|
266
|
-
const installed = await
|
|
249
|
+
const installed = await installSkillsCliDependency({
|
|
267
250
|
cwd: params.cwd,
|
|
268
251
|
configs: params.configs,
|
|
269
252
|
dependency
|
|
270
253
|
})
|
|
271
254
|
const definition = await parseFrontmatterSkill(installed.skillPath)
|
|
272
|
-
const dependencyAsset =
|
|
255
|
+
const dependencyAsset = createResolvedSkillAsset({
|
|
273
256
|
cwd: params.cwd,
|
|
274
257
|
definition
|
|
275
258
|
})
|
|
@@ -334,14 +317,13 @@ export const expandSkillAssetDependenciesWithRegistry = async (
|
|
|
334
317
|
const dependencyAsset = await installDependencyAsset(dependency, asset.instancePath).catch((error: unknown) => {
|
|
335
318
|
if (
|
|
336
319
|
localOrBridgedDependency != null &&
|
|
337
|
-
dependency.source == null
|
|
338
|
-
dependency.registry == null
|
|
320
|
+
dependency.source == null
|
|
339
321
|
) {
|
|
340
322
|
return localOrBridgedDependency
|
|
341
323
|
}
|
|
342
324
|
throw error
|
|
343
325
|
}) ?? (
|
|
344
|
-
dependency.source == null
|
|
326
|
+
dependency.source == null
|
|
345
327
|
? localOrBridgedDependency
|
|
346
328
|
: undefined
|
|
347
329
|
)
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|