@vibe-forge/workspace-assets 2.0.3 → 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.
@@ -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",
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": {
@@ -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",
33
34
  "@vibe-forge/types": "^2.0.2",
34
- "@vibe-forge/utils": "^2.0.3",
35
- "@vibe-forge/definition-core": "^2.0.0"
35
+ "@vibe-forge/utils": "^2.0.4-alpha.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -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 { resolveProjectAiBaseDir, resolveProjectAiEntitiesDir, resolveRelativePath } from '@vibe-forge/utils'
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?.homeBridge
157
- const userHomeBridge = userConfig?.skills?.homeBridge
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
+ }
@@ -35,7 +35,7 @@ import {
35
35
  resolveSelectedSkillAssetsWithDependencies,
36
36
  toDocumentDefinitions
37
37
  } from './selection-internal'
38
- import { expandSkillAssetDependenciesWithRegistry } from './skill-dependencies'
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 expandSkillAssetDependenciesWithRegistry({
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, expandSkillAssetDependenciesWithRegistry } from './skill-dependencies'
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 expandSkillAssetDependenciesWithRegistry({
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 { installRegistrySkillDependency } from './skill-registry'
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 createRegistrySkillAsset = (params: {
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 parseStringDependency(stringValue)
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: buildDependencyRef({
155
+ ref: formatSkillsSpec({
235
156
  name,
236
- ...(source == null ? {} : { source }),
237
- ...(version == null ? {} : { version })
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 expandSkillAssetDependenciesWithRegistry = async (
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 = `${dependency.registry ?? ''}:${dependency.ref}`
246
+ const fetchKey = dependency.ref
325
247
  if (!fetchedDependencyRefs.has(fetchKey)) {
326
248
  fetchedDependencyRefs.add(fetchKey)
327
- const installed = await installRegistrySkillDependency({
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 = createRegistrySkillAsset({
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 && dependency.registry == null
326
+ dependency.source == null
406
327
  ? localOrBridgedDependency
407
328
  : undefined
408
329
  )