@vibe-forge/workspace-assets 0.9.1-alpha.0 → 0.9.2-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/AGENTS.md +4 -1
- package/LICENSE +21 -0
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +360 -355
- package/__tests__/adapter-asset-plan.spec.ts +76 -38
- package/__tests__/bundle.spec.ts +195 -43
- package/__tests__/prompt-builders.spec.ts +206 -0
- package/__tests__/prompt-selection.spec.ts +362 -14
- package/__tests__/selection-internal.spec.ts +42 -0
- package/__tests__/snapshot.ts +78 -128
- package/__tests__/test-helpers.ts +13 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +84 -103
- package/package.json +11 -10
- package/src/adapter-asset-plan.ts +92 -111
- package/src/bundle-internal.ts +548 -0
- package/src/bundle.ts +17 -166
- package/src/internal-types.ts +1 -39
- package/src/prompt-builders.ts +184 -0
- package/src/prompt-selection.ts +144 -104
- package/src/selection-internal.ts +275 -0
- package/src/document-assets.ts +0 -191
- package/src/helpers.ts +0 -35
- package/src/plugin-assets.ts +0 -175
package/src/prompt-selection.ts
CHANGED
|
@@ -1,139 +1,179 @@
|
|
|
1
|
-
import { basename, dirname } from 'node:path'
|
|
2
|
-
|
|
3
|
-
import { DefinitionLoader } from '@vibe-forge/definition-loader'
|
|
4
1
|
import type {
|
|
2
|
+
Definition,
|
|
5
3
|
Filter,
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
PluginOverlayConfig,
|
|
5
|
+
Rule,
|
|
6
|
+
RuleReference,
|
|
7
|
+
SkillSelection,
|
|
8
8
|
WorkspaceAsset,
|
|
9
9
|
WorkspaceAssetBundle,
|
|
10
|
+
WorkspaceMcpSelection,
|
|
10
11
|
WorkspaceSkillSelection
|
|
11
12
|
} from '@vibe-forge/types'
|
|
12
13
|
|
|
14
|
+
import { resolveWorkspaceAssetBundle } from './bundle'
|
|
13
15
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
generateEntitiesRoutePrompt,
|
|
17
|
+
generateRulesPrompt,
|
|
18
|
+
generateSkillsPrompt,
|
|
19
|
+
generateSkillsRoutePrompt,
|
|
20
|
+
generateSpecRoutePrompt
|
|
21
|
+
} from './prompt-builders'
|
|
22
|
+
import {
|
|
23
|
+
definitionWithResolvedName,
|
|
24
|
+
pickDocumentAsset,
|
|
25
|
+
resolveExcludedSkillRefs,
|
|
26
|
+
resolveIncludedSkillRefs,
|
|
27
|
+
resolveNamedAssets,
|
|
28
|
+
resolvePluginOverlay,
|
|
29
|
+
resolveRuleSelection,
|
|
30
|
+
resolveSelectedSkillAssets,
|
|
31
|
+
toDocumentDefinitions
|
|
32
|
+
} from './selection-internal'
|
|
33
|
+
|
|
34
|
+
export async function resolvePromptAssetSelection(params: {
|
|
35
|
+
bundle: WorkspaceAssetBundle
|
|
36
|
+
type: 'spec' | 'entity' | undefined
|
|
37
|
+
name?: string
|
|
38
|
+
input?: {
|
|
39
|
+
skills?: WorkspaceSkillSelection
|
|
40
|
+
}
|
|
41
|
+
}) {
|
|
42
|
+
const options: {
|
|
43
|
+
systemPrompt?: string
|
|
44
|
+
tools?: Filter
|
|
45
|
+
mcpServers?: WorkspaceMcpSelection
|
|
46
|
+
promptAssetIds?: string[]
|
|
47
|
+
assetBundle?: WorkspaceAssetBundle
|
|
48
|
+
} = {
|
|
49
|
+
assetBundle: params.bundle
|
|
34
50
|
}
|
|
35
|
-
): Promise<[PromptAssetResolution, ResolvedPromptAssetOptions]> {
|
|
36
|
-
const loader = new DefinitionLoader(params.bundle.cwd)
|
|
37
|
-
const options: ResolvedPromptAssetOptions = {}
|
|
38
|
-
const systemPromptParts: string[] = []
|
|
39
|
-
|
|
40
|
-
const entities = params.type !== 'entity'
|
|
41
|
-
? toDocumentDefinitions(params.bundle.entities)
|
|
42
|
-
: []
|
|
43
|
-
const skills = toDocumentDefinitions(
|
|
44
|
-
filterSkillAssets(params.bundle.skills, params.input?.skills)
|
|
45
|
-
)
|
|
46
|
-
const rules = toDocumentDefinitions(params.bundle.rules)
|
|
47
|
-
const specs = toDocumentDefinitions(params.bundle.specs)
|
|
48
|
-
|
|
49
|
-
const promptAssetIds = new Set<string>([
|
|
50
|
-
...toPromptAssetIds(params.bundle.rules),
|
|
51
|
-
...(params.type !== 'entity' ? toPromptAssetIds(params.bundle.entities) : []),
|
|
52
|
-
...toPromptAssetIds(params.bundle.specs),
|
|
53
|
-
...toPromptAssetIds(filterSkillAssets(params.bundle.skills, params.input?.skills))
|
|
54
|
-
])
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
let effectiveBundle = params.bundle
|
|
53
|
+
let pinnedTargetAsset: Extract<WorkspaceAsset, { kind: 'spec' | 'entity' }> | undefined
|
|
57
54
|
let targetBody = ''
|
|
58
55
|
let targetToolsFilter: Filter | undefined
|
|
59
56
|
let targetMcpServersFilter: Filter | undefined
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
if (params.input?.skills?.include != null && params.input.skills.include.length > 0) {
|
|
63
|
-
selectedSkillAssets = dedupeSkillAssets(
|
|
64
|
-
filterSkillAssets(params.bundle.skills, { include: params.input.skills.include })
|
|
65
|
-
)
|
|
66
|
-
}
|
|
57
|
+
let targetInstancePath: string | undefined
|
|
67
58
|
|
|
68
59
|
if (params.type && params.name) {
|
|
69
|
-
const
|
|
70
|
-
?
|
|
71
|
-
:
|
|
72
|
-
|
|
73
|
-
if (targetAsset == null) {
|
|
60
|
+
const baseTarget = params.type === 'spec'
|
|
61
|
+
? pickDocumentAsset(params.bundle.specs, params.name)
|
|
62
|
+
: pickDocumentAsset(params.bundle.entities, params.name)
|
|
63
|
+
if (baseTarget == null) {
|
|
74
64
|
throw new Error(`Failed to load ${params.type} ${params.name}`)
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
const pluginOverlay = baseTarget.payload.definition.attributes.plugins as PluginOverlayConfig | undefined
|
|
68
|
+
if (pluginOverlay != null) {
|
|
69
|
+
effectiveBundle = await resolveWorkspaceAssetBundle({
|
|
70
|
+
cwd: params.bundle.cwd,
|
|
71
|
+
plugins: resolvePluginOverlay(params.bundle.pluginConfigs, pluginOverlay),
|
|
72
|
+
overlaySource: `${params.type}:${baseTarget.displayName}`
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pinnedTargetAsset = baseTarget
|
|
77
|
+
targetBody = baseTarget.payload.definition.body
|
|
78
|
+
targetToolsFilter = baseTarget.payload.definition.attributes.tools
|
|
79
|
+
targetMcpServersFilter = baseTarget.payload.definition.attributes.mcpServers
|
|
80
|
+
targetInstancePath = baseTarget.instancePath
|
|
81
|
+
options.assetBundle = effectiveBundle
|
|
82
|
+
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
const selectedSkillAssets = resolveSelectedSkillAssets(effectiveBundle.skills, params.input?.skills)
|
|
85
|
+
const promptAssetIds = new Set<string>([
|
|
86
|
+
...effectiveBundle.rules.map(asset => asset.id),
|
|
87
|
+
...effectiveBundle.specs.map(asset => asset.id),
|
|
88
|
+
...selectedSkillAssets.map(asset => asset.id),
|
|
89
|
+
...(params.type !== 'entity' ? effectiveBundle.entities.map(asset => asset.id) : [])
|
|
90
|
+
])
|
|
91
|
+
const ruleDefinitions = new Map<string, Definition<Rule>>(
|
|
92
|
+
effectiveBundle.rules.map(asset => [
|
|
93
|
+
asset.id,
|
|
94
|
+
definitionWithResolvedName(asset.payload.definition, asset.displayName, asset.instancePath)
|
|
95
|
+
])
|
|
96
|
+
)
|
|
97
|
+
const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
|
|
98
|
+
|
|
99
|
+
if (pinnedTargetAsset != null) {
|
|
100
|
+
promptAssetIds.add(pinnedTargetAsset.id)
|
|
101
|
+
const attributes = pinnedTargetAsset.payload.definition.attributes
|
|
102
|
+
|
|
103
|
+
if (attributes.rules != null) {
|
|
104
|
+
const selection = await resolveRuleSelection(
|
|
105
|
+
effectiveBundle,
|
|
106
|
+
attributes.rules as RuleReference[] | string[],
|
|
107
|
+
targetInstancePath
|
|
91
108
|
)
|
|
92
|
-
for (const asset of
|
|
109
|
+
for (const asset of selection.assets) {
|
|
93
110
|
promptAssetIds.add(asset.id)
|
|
111
|
+
ruleDefinitions.set(
|
|
112
|
+
asset.id,
|
|
113
|
+
definitionWithResolvedName(
|
|
114
|
+
{
|
|
115
|
+
...asset.payload.definition,
|
|
116
|
+
attributes: {
|
|
117
|
+
...asset.payload.definition.attributes,
|
|
118
|
+
always: true
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
asset.displayName,
|
|
122
|
+
asset.instancePath
|
|
123
|
+
)
|
|
124
|
+
)
|
|
94
125
|
}
|
|
126
|
+
selection.remoteDefinitions.forEach((definition) => {
|
|
127
|
+
ruleDefinitions.set(definition.path, definition)
|
|
128
|
+
})
|
|
95
129
|
}
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
const skillSelection = attributes.skills as string[] | SkillSelection | undefined
|
|
132
|
+
const includedRefs = resolveIncludedSkillRefs(skillSelection)
|
|
133
|
+
const excludedRefs = resolveExcludedSkillRefs(skillSelection)
|
|
134
|
+
const includedAssets = skillSelection == null
|
|
135
|
+
? []
|
|
136
|
+
: includedRefs != null
|
|
137
|
+
? (includedRefs.length > 0 ? resolveNamedAssets(effectiveBundle.skills, includedRefs, targetInstancePath) : [])
|
|
138
|
+
: effectiveBundle.skills
|
|
139
|
+
const excludedIds = new Set(
|
|
140
|
+
resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
|
|
141
|
+
)
|
|
108
142
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
includedAssets
|
|
144
|
+
.filter(asset => !excludedIds.has(asset.id))
|
|
145
|
+
.forEach((asset) => {
|
|
146
|
+
targetSkillsAssets.push(asset)
|
|
147
|
+
promptAssetIds.add(asset.id)
|
|
148
|
+
})
|
|
112
149
|
}
|
|
113
150
|
|
|
151
|
+
const rules = Array.from(ruleDefinitions.values())
|
|
114
152
|
const targetSkills = toDocumentDefinitions(targetSkillsAssets)
|
|
115
|
-
const
|
|
116
|
-
selectedSkillAssets.filter(
|
|
117
|
-
skill => !targetSkillsAssets.some(target => target.payload.definition.path === skill.payload.definition.path)
|
|
118
|
-
)
|
|
153
|
+
const routedSkills = toDocumentDefinitions(
|
|
154
|
+
selectedSkillAssets.filter(skill => !targetSkillsAssets.some(target => target.id === skill.id))
|
|
119
155
|
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
const entities = params.type !== 'entity'
|
|
157
|
+
? toDocumentDefinitions(effectiveBundle.entities)
|
|
158
|
+
: []
|
|
159
|
+
const skills = toDocumentDefinitions(selectedSkillAssets)
|
|
160
|
+
const specs = toDocumentDefinitions(effectiveBundle.specs)
|
|
161
|
+
|
|
162
|
+
options.systemPrompt = [
|
|
163
|
+
generateRulesPrompt(effectiveBundle.cwd, rules),
|
|
164
|
+
generateSkillsPrompt(effectiveBundle.cwd, targetSkills),
|
|
165
|
+
generateEntitiesRoutePrompt(entities),
|
|
166
|
+
generateSkillsRoutePrompt(effectiveBundle.cwd, routedSkills),
|
|
167
|
+
generateSpecRoutePrompt(specs, { active: params.type === 'spec' }),
|
|
168
|
+
targetBody
|
|
169
|
+
].join('\n\n')
|
|
170
|
+
|
|
171
|
+
if (targetToolsFilter != null) {
|
|
130
172
|
options.tools = targetToolsFilter
|
|
131
173
|
}
|
|
132
|
-
if (targetMcpServersFilter) {
|
|
174
|
+
if (targetMcpServersFilter != null) {
|
|
133
175
|
options.mcpServers = targetMcpServersFilter
|
|
134
176
|
}
|
|
135
|
-
|
|
136
|
-
options.systemPrompt = systemPromptParts.join('\n\n')
|
|
137
177
|
options.promptAssetIds = Array.from(promptAssetIds)
|
|
138
178
|
|
|
139
179
|
return [
|
|
@@ -147,5 +187,5 @@ export async function resolvePromptAssetSelection(
|
|
|
147
187
|
promptAssetIds: Array.from(promptAssetIds)
|
|
148
188
|
},
|
|
149
189
|
options
|
|
150
|
-
]
|
|
190
|
+
] as const
|
|
151
191
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Definition,
|
|
3
|
+
PluginConfig,
|
|
4
|
+
PluginOverlayConfig,
|
|
5
|
+
Rule,
|
|
6
|
+
RuleReference,
|
|
7
|
+
SkillSelection,
|
|
8
|
+
WorkspaceAsset,
|
|
9
|
+
WorkspaceAssetBundle,
|
|
10
|
+
WorkspaceAssetKind,
|
|
11
|
+
WorkspaceMcpSelection,
|
|
12
|
+
WorkspaceSkillSelection
|
|
13
|
+
} from '@vibe-forge/types'
|
|
14
|
+
import { normalizePath } from '@vibe-forge/utils'
|
|
15
|
+
import { mergePluginConfigs, normalizePluginConfig } from '@vibe-forge/utils/plugin-resolver'
|
|
16
|
+
import { glob } from 'fast-glob'
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createRemoteRuleDefinition,
|
|
20
|
+
isLocalRuleReference,
|
|
21
|
+
isPathLikeReference,
|
|
22
|
+
isRemoteRuleReference,
|
|
23
|
+
parseScopedReference
|
|
24
|
+
} from '@vibe-forge/definition-core'
|
|
25
|
+
|
|
26
|
+
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
27
|
+
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
28
|
+
payload: {
|
|
29
|
+
definition: TDefinition & { path: string }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ASSET_REFERENCE_PATH_SUFFIXES = ['.md', '.json', '.yaml', '.yml']
|
|
34
|
+
|
|
35
|
+
export const definitionWithResolvedName = <TDefinition>(
|
|
36
|
+
definition: Definition<TDefinition>,
|
|
37
|
+
resolvedName: string,
|
|
38
|
+
instancePath?: string
|
|
39
|
+
) => ({
|
|
40
|
+
...definition,
|
|
41
|
+
resolvedName,
|
|
42
|
+
resolvedInstancePath: instancePath
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const toDocumentDefinitions = <TDefinition>(
|
|
46
|
+
assets: Array<DocumentAsset<TDefinition>>
|
|
47
|
+
) =>
|
|
48
|
+
assets.map(asset =>
|
|
49
|
+
definitionWithResolvedName(
|
|
50
|
+
asset.payload.definition,
|
|
51
|
+
asset.displayName,
|
|
52
|
+
asset.instancePath
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const resolveUniqueAssetByName = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
57
|
+
assets: TAsset[],
|
|
58
|
+
name: string
|
|
59
|
+
) => {
|
|
60
|
+
const matches = assets.filter(asset => asset.name === name)
|
|
61
|
+
if (matches.length === 0) return undefined
|
|
62
|
+
const unscopedMatches = matches.filter(asset => asset.scope == null)
|
|
63
|
+
if (unscopedMatches.length === 1) {
|
|
64
|
+
return unscopedMatches[0]
|
|
65
|
+
}
|
|
66
|
+
if (matches.length > 1) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Ambiguous asset reference ${name}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return matches[0]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resolveScopedAsset = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
75
|
+
assets: TAsset[],
|
|
76
|
+
scope: string,
|
|
77
|
+
name: string
|
|
78
|
+
) => assets.find(asset => asset.scope === scope && asset.name === name)
|
|
79
|
+
|
|
80
|
+
export const findNamedAsset = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
81
|
+
assets: TAsset[],
|
|
82
|
+
ref: string,
|
|
83
|
+
currentInstancePath?: string
|
|
84
|
+
) => {
|
|
85
|
+
const scoped = parseScopedReference(ref, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES })
|
|
86
|
+
if (scoped != null) {
|
|
87
|
+
return resolveScopedAsset(assets, scoped.scope, scoped.name)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (currentInstancePath != null) {
|
|
91
|
+
const local = assets.find(asset => asset.instancePath === currentInstancePath && asset.name === ref)
|
|
92
|
+
if (local != null) {
|
|
93
|
+
return local
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return resolveUniqueAssetByName(assets, ref)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
101
|
+
assets: TAsset[],
|
|
102
|
+
refs: string[] | undefined,
|
|
103
|
+
currentInstancePath?: string
|
|
104
|
+
) => {
|
|
105
|
+
if (refs == null || refs.length === 0) return [] as TAsset[]
|
|
106
|
+
|
|
107
|
+
const selected: TAsset[] = []
|
|
108
|
+
const seen = new Set<string>()
|
|
109
|
+
|
|
110
|
+
const add = (asset: TAsset) => {
|
|
111
|
+
if (seen.has(asset.id)) return
|
|
112
|
+
seen.add(asset.id)
|
|
113
|
+
selected.push(asset)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const ref of refs) {
|
|
117
|
+
const asset = findNamedAsset(assets, ref, currentInstancePath)
|
|
118
|
+
if (asset == null) throw new Error(`Failed to resolve asset ${ref}`)
|
|
119
|
+
add(asset)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return selected
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const resolvePathMatchedRules = async (
|
|
126
|
+
bundle: WorkspaceAssetBundle,
|
|
127
|
+
ref: string
|
|
128
|
+
) => {
|
|
129
|
+
const matchedPaths = new Set(
|
|
130
|
+
(await glob(ref, {
|
|
131
|
+
cwd: bundle.cwd,
|
|
132
|
+
absolute: true
|
|
133
|
+
})).map(normalizePath)
|
|
134
|
+
)
|
|
135
|
+
return bundle.rules.filter(rule => matchedPaths.has(normalizePath(rule.sourcePath)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const resolveRuleSelection = async (
|
|
139
|
+
bundle: WorkspaceAssetBundle,
|
|
140
|
+
refs: RuleReference[] | string[] | undefined,
|
|
141
|
+
currentInstancePath?: string
|
|
142
|
+
) => {
|
|
143
|
+
const assets: Array<Extract<WorkspaceAsset, { kind: 'rule' }>> = []
|
|
144
|
+
const remoteDefinitions: Definition<Rule>[] = []
|
|
145
|
+
const seen = new Set<string>()
|
|
146
|
+
|
|
147
|
+
const addAsset = (asset: Extract<WorkspaceAsset, { kind: 'rule' }>) => {
|
|
148
|
+
if (seen.has(asset.id)) return
|
|
149
|
+
seen.add(asset.id)
|
|
150
|
+
assets.push(asset)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let remoteIndex = 0
|
|
154
|
+
for (const ref of refs ?? []) {
|
|
155
|
+
if (isRemoteRuleReference(ref)) {
|
|
156
|
+
remoteDefinitions.push(createRemoteRuleDefinition(ref, remoteIndex++))
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const value = typeof ref === 'string'
|
|
161
|
+
? ref
|
|
162
|
+
: isLocalRuleReference(ref)
|
|
163
|
+
? ref.path
|
|
164
|
+
: undefined
|
|
165
|
+
if (value == null) continue
|
|
166
|
+
if (
|
|
167
|
+
isPathLikeReference(value, {
|
|
168
|
+
pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES,
|
|
169
|
+
allowGlob: true
|
|
170
|
+
})
|
|
171
|
+
) {
|
|
172
|
+
const matched = await resolvePathMatchedRules(bundle, value)
|
|
173
|
+
matched.forEach(addAsset)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const asset = findNamedAsset(bundle.rules, value, currentInstancePath)
|
|
178
|
+
if (asset == null) throw new Error(`Failed to resolve rule ${value}`)
|
|
179
|
+
addAsset(asset)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
assets,
|
|
184
|
+
remoteDefinitions
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const resolveIncludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
|
|
189
|
+
if (selection == null) return undefined
|
|
190
|
+
if (Array.isArray(selection)) return selection
|
|
191
|
+
return selection.type === 'include' ? selection.list : undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const resolveExcludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
|
|
195
|
+
if (selection == null || Array.isArray(selection)) return undefined
|
|
196
|
+
return selection.type === 'exclude' ? selection.list : undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const resolveSelectedSkillAssets = (
|
|
200
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
|
|
201
|
+
selection?: WorkspaceSkillSelection
|
|
202
|
+
) => {
|
|
203
|
+
if (selection == null) return assets
|
|
204
|
+
|
|
205
|
+
const included = selection.include != null && selection.include.length > 0
|
|
206
|
+
? resolveNamedAssets(assets, selection.include)
|
|
207
|
+
: assets
|
|
208
|
+
const excluded = new Set(
|
|
209
|
+
resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
|
|
210
|
+
)
|
|
211
|
+
return included.filter(asset => !excluded.has(asset.id))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const resolveSelectedMcpNames = (
|
|
215
|
+
bundle: WorkspaceAssetBundle,
|
|
216
|
+
selection: WorkspaceMcpSelection | undefined
|
|
217
|
+
) => {
|
|
218
|
+
const allAssets = Object.values(bundle.mcpServers)
|
|
219
|
+
const includeRefs = selection?.include ??
|
|
220
|
+
(bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined)
|
|
221
|
+
const excludeRefs = selection?.exclude ??
|
|
222
|
+
(
|
|
223
|
+
selection?.include == null && bundle.defaultExcludeMcpServers.length > 0
|
|
224
|
+
? bundle.defaultExcludeMcpServers
|
|
225
|
+
: undefined
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const resolveRefs = (refs: string[] | undefined) => {
|
|
229
|
+
if (refs == null || refs.length === 0) return undefined
|
|
230
|
+
return new Set(refs.map((ref) => {
|
|
231
|
+
const scoped = parseScopedReference(ref, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES })
|
|
232
|
+
if (scoped != null) {
|
|
233
|
+
const asset = allAssets.find(item => item.scope === scoped.scope && item.name === scoped.name)
|
|
234
|
+
if (asset == null) throw new Error(`Failed to resolve MCP server ${ref}`)
|
|
235
|
+
return asset.displayName
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const matches = allAssets.filter(item => item.name === ref || item.displayName === ref)
|
|
239
|
+
if (matches.length === 0) throw new Error(`Failed to resolve MCP server ${ref}`)
|
|
240
|
+
if (matches.length > 1) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Ambiguous MCP server reference ${ref}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
return matches[0].displayName
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const include = resolveRefs(includeRefs)
|
|
250
|
+
const exclude = resolveRefs(excludeRefs) ?? new Set<string>()
|
|
251
|
+
|
|
252
|
+
return allAssets
|
|
253
|
+
.map(asset => asset.displayName)
|
|
254
|
+
.filter(name => (include == null || include.has(name)) && !exclude.has(name))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const resolvePluginOverlay = (
|
|
258
|
+
basePlugins: PluginConfig | undefined,
|
|
259
|
+
overlay: PluginOverlayConfig | undefined
|
|
260
|
+
) => {
|
|
261
|
+
if (overlay == null) return basePlugins
|
|
262
|
+
if (overlay.mode !== 'override' && overlay.mode !== 'extend') {
|
|
263
|
+
throw new Error('Invalid plugins overlay. "mode" must be "extend" or "override".')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const overlayList = normalizePluginConfig(overlay.list, 'plugins overlay list') ?? []
|
|
267
|
+
return overlay.mode === 'override'
|
|
268
|
+
? overlayList
|
|
269
|
+
: mergePluginConfigs(basePlugins, overlayList)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const pickDocumentAsset = <TAsset extends Extract<WorkspaceAsset, { kind: 'spec' | 'entity' }>>(
|
|
273
|
+
assets: TAsset[],
|
|
274
|
+
ref: string
|
|
275
|
+
) => findNamedAsset(assets, ref)
|