@vibe-forge/workspace-assets 0.9.0 → 0.9.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/AGENTS.md +4 -1
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +1 -1
- package/__tests__/adapter-asset-plan.spec.ts +24 -12
- package/__tests__/bundle.spec.ts +59 -31
- package/__tests__/prompt-builders.spec.ts +198 -0
- package/__tests__/prompt-selection.spec.ts +26 -22
- package/__tests__/snapshot.ts +4 -2
- package/__tests__/test-helpers.ts +5 -3
- package/__tests__/workspace-assets.snapshot.spec.ts +24 -12
- package/package.json +4 -3
- package/src/adapter-asset-plan.ts +155 -0
- package/src/bundle-internal.ts +548 -0
- package/src/bundle.ts +29 -0
- package/src/index.ts +3 -1368
- package/src/prompt-builders.ts +184 -0
- package/src/prompt-selection.ts +191 -0
- package/src/selection-internal.ts +271 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdapterAssetPlan,
|
|
5
|
+
AdapterOverlayEntry,
|
|
6
|
+
AssetDiagnostic,
|
|
7
|
+
WorkspaceAssetAdapter,
|
|
8
|
+
WorkspaceAssetBundle,
|
|
9
|
+
WorkspaceMcpSelection,
|
|
10
|
+
WorkspaceSkillSelection
|
|
11
|
+
} from '@vibe-forge/types'
|
|
12
|
+
|
|
13
|
+
import { resolveSelectedMcpNames, resolveSelectedSkillAssets } from './selection-internal'
|
|
14
|
+
|
|
15
|
+
export function buildAdapterAssetPlan(params: {
|
|
16
|
+
adapter: WorkspaceAssetAdapter
|
|
17
|
+
bundle: WorkspaceAssetBundle
|
|
18
|
+
options: {
|
|
19
|
+
mcpServers?: WorkspaceMcpSelection
|
|
20
|
+
skills?: WorkspaceSkillSelection
|
|
21
|
+
promptAssetIds?: string[]
|
|
22
|
+
}
|
|
23
|
+
}): AdapterAssetPlan {
|
|
24
|
+
const diagnostics: AssetDiagnostic[] = []
|
|
25
|
+
|
|
26
|
+
for (const assetId of params.options.promptAssetIds ?? []) {
|
|
27
|
+
const asset = params.bundle.assets.find(item => item.id === assetId)
|
|
28
|
+
if (asset == null || asset.kind === 'mcpServer') continue
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
assetId,
|
|
31
|
+
adapter: params.adapter,
|
|
32
|
+
status: 'prompt',
|
|
33
|
+
reason: 'Mapped into the generated system prompt.',
|
|
34
|
+
packageId: asset.packageId,
|
|
35
|
+
scope: asset.scope,
|
|
36
|
+
instancePath: asset.instancePath,
|
|
37
|
+
origin: asset.origin,
|
|
38
|
+
resolvedBy: asset.resolvedBy,
|
|
39
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const selectedMcpNames = resolveSelectedMcpNames(params.bundle, params.options.mcpServers)
|
|
44
|
+
const mcpServers = Object.fromEntries(
|
|
45
|
+
selectedMcpNames.map(name => [name, params.bundle.mcpServers[name].payload.config])
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
selectedMcpNames.forEach((name) => {
|
|
49
|
+
const asset = params.bundle.mcpServers[name]
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
assetId: asset.id,
|
|
52
|
+
adapter: params.adapter,
|
|
53
|
+
status: params.adapter === 'claude-code' ? 'native' : 'translated',
|
|
54
|
+
reason: params.adapter === 'claude-code'
|
|
55
|
+
? 'Mapped into adapter MCP settings.'
|
|
56
|
+
: 'Translated into adapter-specific MCP configuration.',
|
|
57
|
+
packageId: asset.packageId,
|
|
58
|
+
scope: asset.scope,
|
|
59
|
+
instancePath: asset.instancePath,
|
|
60
|
+
origin: asset.origin,
|
|
61
|
+
resolvedBy: asset.resolvedBy,
|
|
62
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
params.bundle.hookPlugins.forEach((asset) => {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
assetId: asset.id,
|
|
69
|
+
adapter: params.adapter,
|
|
70
|
+
status: 'native',
|
|
71
|
+
reason: params.adapter === 'claude-code'
|
|
72
|
+
? 'Mapped into the Claude Code native hooks bridge.'
|
|
73
|
+
: params.adapter === 'codex'
|
|
74
|
+
? 'Mapped into the Codex native hooks bridge.'
|
|
75
|
+
: 'Mapped into the OpenCode native hooks bridge.',
|
|
76
|
+
packageId: asset.packageId,
|
|
77
|
+
scope: asset.scope,
|
|
78
|
+
instancePath: asset.instancePath,
|
|
79
|
+
origin: asset.origin,
|
|
80
|
+
resolvedBy: asset.resolvedBy,
|
|
81
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const selectedSkillAssets = resolveSelectedSkillAssets(params.bundle.skills, params.options.skills)
|
|
86
|
+
if (params.adapter === 'opencode') {
|
|
87
|
+
selectedSkillAssets.forEach((asset) => {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
assetId: asset.id,
|
|
90
|
+
adapter: params.adapter,
|
|
91
|
+
status: 'native',
|
|
92
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native skill.',
|
|
93
|
+
packageId: asset.packageId,
|
|
94
|
+
scope: asset.scope,
|
|
95
|
+
instancePath: asset.instancePath,
|
|
96
|
+
origin: asset.origin,
|
|
97
|
+
resolvedBy: asset.resolvedBy,
|
|
98
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
assetId: asset.id,
|
|
104
|
+
adapter: params.adapter,
|
|
105
|
+
status: 'native',
|
|
106
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.',
|
|
107
|
+
packageId: asset.packageId,
|
|
108
|
+
scope: asset.scope,
|
|
109
|
+
instancePath: asset.instancePath,
|
|
110
|
+
origin: asset.origin,
|
|
111
|
+
resolvedBy: asset.resolvedBy,
|
|
112
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
} else if (params.adapter === 'codex') {
|
|
116
|
+
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
assetId: asset.id,
|
|
119
|
+
adapter: params.adapter,
|
|
120
|
+
status: 'skipped',
|
|
121
|
+
reason: 'No stable native Codex mapping exists for this asset kind in V1.',
|
|
122
|
+
packageId: asset.packageId,
|
|
123
|
+
scope: asset.scope,
|
|
124
|
+
instancePath: asset.instancePath,
|
|
125
|
+
origin: asset.origin,
|
|
126
|
+
resolvedBy: asset.resolvedBy,
|
|
127
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const overlays: AdapterOverlayEntry[] = params.adapter === 'opencode'
|
|
133
|
+
? [
|
|
134
|
+
...selectedSkillAssets.map((asset): AdapterOverlayEntry => ({
|
|
135
|
+
assetId: asset.id,
|
|
136
|
+
kind: 'skill',
|
|
137
|
+
sourcePath: dirname(asset.sourcePath),
|
|
138
|
+
targetPath: `skills/${asset.displayName.replaceAll('/', '__')}`
|
|
139
|
+
})),
|
|
140
|
+
...params.bundle.opencodeOverlayAssets.map((asset): AdapterOverlayEntry => ({
|
|
141
|
+
assetId: asset.id,
|
|
142
|
+
kind: asset.kind,
|
|
143
|
+
sourcePath: asset.sourcePath,
|
|
144
|
+
targetPath: asset.payload.targetSubpath
|
|
145
|
+
}))
|
|
146
|
+
]
|
|
147
|
+
: []
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
adapter: params.adapter,
|
|
151
|
+
diagnostics,
|
|
152
|
+
mcpServers,
|
|
153
|
+
overlays
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, dirname, extname, resolve } from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
7
|
+
buildConfigJsonVariables,
|
|
8
|
+
loadConfig,
|
|
9
|
+
resolveDefaultVibeForgeMcpServerConfig
|
|
10
|
+
} from '@vibe-forge/config'
|
|
11
|
+
import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
|
|
12
|
+
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
13
|
+
import {
|
|
14
|
+
flattenPluginInstances,
|
|
15
|
+
mergePluginConfigs,
|
|
16
|
+
resolveConfiguredPluginInstances,
|
|
17
|
+
resolvePluginHooksEntryPath
|
|
18
|
+
} from '@vibe-forge/utils/plugin-resolver'
|
|
19
|
+
import type { ResolvedPluginInstance } from '@vibe-forge/utils/plugin-resolver'
|
|
20
|
+
import { glob } from 'fast-glob'
|
|
21
|
+
import fm from 'front-matter'
|
|
22
|
+
import yaml from 'js-yaml'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
resolveDocumentName,
|
|
26
|
+
resolveEntityIdentifier,
|
|
27
|
+
resolveSkillIdentifier,
|
|
28
|
+
resolveSpecIdentifier
|
|
29
|
+
} from '@vibe-forge/definition-core'
|
|
30
|
+
|
|
31
|
+
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
32
|
+
type OpenCodeOverlayKind = Extract<WorkspaceAssetKind, 'agent' | 'command' | 'mode' | 'nativePlugin'>
|
|
33
|
+
type OpenCodeOverlayAsset<TKind extends OpenCodeOverlayKind> = Extract<WorkspaceAsset, { kind: TKind }>
|
|
34
|
+
|
|
35
|
+
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
36
|
+
payload: {
|
|
37
|
+
definition: TDefinition & { path: string }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface OpenCodeOverlayAssetEntry {
|
|
42
|
+
kind: OpenCodeOverlayKind
|
|
43
|
+
sourcePath: string
|
|
44
|
+
entryName: string
|
|
45
|
+
targetSubpath: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
49
|
+
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const resolveDisplayName = (name: string, scope?: string) => (
|
|
53
|
+
scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const loadWorkspaceConfig = async (cwd: string) => (
|
|
57
|
+
loadConfig({
|
|
58
|
+
cwd,
|
|
59
|
+
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const parseFrontmatterDocument = async <TDefinition extends object>(
|
|
64
|
+
path: string
|
|
65
|
+
): Promise<Definition<TDefinition>> => {
|
|
66
|
+
const content = await readFile(path, 'utf-8')
|
|
67
|
+
const { body, attributes } = fm<TDefinition>(content)
|
|
68
|
+
return {
|
|
69
|
+
path,
|
|
70
|
+
body,
|
|
71
|
+
attributes
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> => {
|
|
76
|
+
const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>
|
|
77
|
+
const promptPath = typeof raw.promptPath === 'string'
|
|
78
|
+
? (raw.promptPath.startsWith('/') ? raw.promptPath : resolve(dirname(path), raw.promptPath))
|
|
79
|
+
: undefined
|
|
80
|
+
const prompt = typeof raw.prompt === 'string'
|
|
81
|
+
? raw.prompt
|
|
82
|
+
: promptPath != null
|
|
83
|
+
? await readFile(promptPath, 'utf-8')
|
|
84
|
+
: ''
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
path,
|
|
88
|
+
body: prompt,
|
|
89
|
+
attributes: raw as Entity
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parseStructuredMcpFile = async (path: string) => {
|
|
94
|
+
const raw = await readFile(path, 'utf8')
|
|
95
|
+
const extension = extname(path).toLowerCase()
|
|
96
|
+
return extension === '.yaml' || extension === '.yml'
|
|
97
|
+
? yaml.load(raw)
|
|
98
|
+
: JSON.parse(raw)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const createDocumentAsset = <
|
|
102
|
+
TKind extends DocumentAssetKind,
|
|
103
|
+
TDefinition extends { path: string; attributes: { name?: string } },
|
|
104
|
+
>(params: {
|
|
105
|
+
cwd: string
|
|
106
|
+
kind: TKind
|
|
107
|
+
definition: TDefinition
|
|
108
|
+
origin: 'workspace' | 'plugin'
|
|
109
|
+
scope?: string
|
|
110
|
+
instance?: ResolvedPluginInstance
|
|
111
|
+
}) => {
|
|
112
|
+
const name = ({
|
|
113
|
+
rule: resolveDocumentName,
|
|
114
|
+
spec: resolveSpecIdentifier,
|
|
115
|
+
entity: resolveEntityIdentifier,
|
|
116
|
+
skill: resolveSkillIdentifier
|
|
117
|
+
}[params.kind])(params.definition.path, params.definition.attributes.name)
|
|
118
|
+
const displayName = resolveDisplayName(name, params.scope)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
id: `${params.kind}:${params.origin}:${params.instance?.instancePath ?? 'workspace'}:${displayName}:${
|
|
122
|
+
resolveRelativePath(params.cwd, params.definition.path)
|
|
123
|
+
}`,
|
|
124
|
+
kind: params.kind,
|
|
125
|
+
name,
|
|
126
|
+
displayName,
|
|
127
|
+
scope: params.scope,
|
|
128
|
+
origin: params.origin,
|
|
129
|
+
sourcePath: params.definition.path,
|
|
130
|
+
instancePath: params.instance?.instancePath,
|
|
131
|
+
packageId: params.instance?.packageId,
|
|
132
|
+
resolvedBy: params.instance?.resolvedBy,
|
|
133
|
+
taskOverlaySource: params.instance?.overlaySource,
|
|
134
|
+
payload: {
|
|
135
|
+
definition: params.definition
|
|
136
|
+
}
|
|
137
|
+
} as Extract<WorkspaceAsset, { kind: TKind }>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const createMcpAsset = (params: {
|
|
141
|
+
cwd: string
|
|
142
|
+
name: string
|
|
143
|
+
config: NonNullable<Config['mcpServers']>[string]
|
|
144
|
+
origin: 'workspace' | 'plugin'
|
|
145
|
+
scope?: string
|
|
146
|
+
sourcePath: string
|
|
147
|
+
instance?: ResolvedPluginInstance
|
|
148
|
+
}) => {
|
|
149
|
+
const displayName = resolveDisplayName(params.name, params.scope)
|
|
150
|
+
return {
|
|
151
|
+
id: `mcpServer:${params.origin}:${params.instance?.instancePath ?? 'workspace'}:${displayName}:${
|
|
152
|
+
resolveRelativePath(params.cwd, params.sourcePath)
|
|
153
|
+
}`,
|
|
154
|
+
kind: 'mcpServer',
|
|
155
|
+
name: params.name,
|
|
156
|
+
displayName,
|
|
157
|
+
scope: params.scope,
|
|
158
|
+
origin: params.origin,
|
|
159
|
+
sourcePath: params.sourcePath,
|
|
160
|
+
instancePath: params.instance?.instancePath,
|
|
161
|
+
packageId: params.instance?.packageId,
|
|
162
|
+
resolvedBy: params.instance?.resolvedBy,
|
|
163
|
+
taskOverlaySource: params.instance?.overlaySource,
|
|
164
|
+
payload: {
|
|
165
|
+
name: displayName,
|
|
166
|
+
config: params.config
|
|
167
|
+
}
|
|
168
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const createHookPluginAsset = (
|
|
172
|
+
instance: ResolvedPluginInstance
|
|
173
|
+
) => ({
|
|
174
|
+
id: `hookPlugin:${instance.instancePath}:${instance.packageId ?? instance.requestId}`,
|
|
175
|
+
kind: 'hookPlugin',
|
|
176
|
+
name: instance.requestId,
|
|
177
|
+
displayName: resolveDisplayName(instance.requestId, instance.scope),
|
|
178
|
+
scope: instance.scope,
|
|
179
|
+
origin: 'plugin' as const,
|
|
180
|
+
sourcePath: instance.rootDir,
|
|
181
|
+
instancePath: instance.instancePath,
|
|
182
|
+
packageId: instance.packageId,
|
|
183
|
+
resolvedBy: instance.resolvedBy,
|
|
184
|
+
taskOverlaySource: instance.overlaySource,
|
|
185
|
+
payload: {
|
|
186
|
+
packageName: instance.packageId,
|
|
187
|
+
config: instance.options
|
|
188
|
+
}
|
|
189
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>)
|
|
190
|
+
|
|
191
|
+
const createOpenCodeOverlayAsset = <TKind extends OpenCodeOverlayKind>(params: {
|
|
192
|
+
cwd: string
|
|
193
|
+
kind: TKind
|
|
194
|
+
sourcePath: string
|
|
195
|
+
entryName: string
|
|
196
|
+
targetSubpath: string
|
|
197
|
+
instance: ResolvedPluginInstance
|
|
198
|
+
}): OpenCodeOverlayAsset<TKind> => ({
|
|
199
|
+
id: `${params.kind}:plugin:${params.instance.instancePath}:${
|
|
200
|
+
resolveDisplayName(params.entryName, params.instance.scope)
|
|
201
|
+
}:${resolveRelativePath(params.cwd, params.sourcePath)}`,
|
|
202
|
+
kind: params.kind,
|
|
203
|
+
name: params.entryName,
|
|
204
|
+
displayName: resolveDisplayName(params.entryName, params.instance.scope),
|
|
205
|
+
scope: params.instance.scope,
|
|
206
|
+
origin: 'plugin' as const,
|
|
207
|
+
sourcePath: params.sourcePath,
|
|
208
|
+
instancePath: params.instance.instancePath,
|
|
209
|
+
packageId: params.instance.packageId,
|
|
210
|
+
resolvedBy: params.instance.resolvedBy,
|
|
211
|
+
taskOverlaySource: params.instance.overlaySource,
|
|
212
|
+
payload: {
|
|
213
|
+
entryName: params.entryName,
|
|
214
|
+
targetSubpath: params.targetSubpath
|
|
215
|
+
}
|
|
216
|
+
} as OpenCodeOverlayAsset<TKind>)
|
|
217
|
+
|
|
218
|
+
const scanWorkspaceDocuments = async (cwd: string) => {
|
|
219
|
+
const [rulePaths, skillPaths, specPaths, entityDocPaths, entityJsonPaths, mcpPaths] = await Promise.all([
|
|
220
|
+
glob(['.ai/rules/*.md'], { cwd, absolute: true }),
|
|
221
|
+
glob(['.ai/skills/*/SKILL.md'], { cwd, absolute: true }),
|
|
222
|
+
glob(['.ai/specs/*.md', '.ai/specs/*/index.md'], { cwd, absolute: true }),
|
|
223
|
+
glob(['.ai/entities/*.md', '.ai/entities/*/README.md'], { cwd, absolute: true }),
|
|
224
|
+
glob(['.ai/entities/*/index.json'], { cwd, absolute: true }),
|
|
225
|
+
glob(['.ai/mcp/*.json', '.ai/mcp/*.yaml', '.ai/mcp/*.yml'], { cwd, absolute: true })
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
rulePaths,
|
|
230
|
+
skillPaths,
|
|
231
|
+
specPaths,
|
|
232
|
+
entityDocPaths,
|
|
233
|
+
entityJsonPaths,
|
|
234
|
+
mcpPaths
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
|
|
239
|
+
const assets = instance.manifest?.assets
|
|
240
|
+
const resolveAssetRoot = (dir: string | undefined, fallback: string) => resolve(instance.rootDir, dir ?? fallback)
|
|
241
|
+
|
|
242
|
+
const [rulePaths, skillPaths, specPaths, entityDocPaths, entityJsonPaths, mcpPaths] = await Promise.all([
|
|
243
|
+
glob(['*.md'], { cwd: resolveAssetRoot(assets?.rules, 'rules'), absolute: true }).catch(() => [] as string[]),
|
|
244
|
+
glob(['*/SKILL.md'], { cwd: resolveAssetRoot(assets?.skills, 'skills'), absolute: true }).catch(() =>
|
|
245
|
+
[] as string[]
|
|
246
|
+
),
|
|
247
|
+
glob(['*.md', '*/index.md'], { cwd: resolveAssetRoot(assets?.specs, 'specs'), absolute: true }).catch(() =>
|
|
248
|
+
[] as string[]
|
|
249
|
+
),
|
|
250
|
+
glob(['*.md', '*/README.md'], { cwd: resolveAssetRoot(assets?.entities, 'entities'), absolute: true }).catch(() =>
|
|
251
|
+
[] as string[]
|
|
252
|
+
),
|
|
253
|
+
glob(['*/index.json'], { cwd: resolveAssetRoot(assets?.entities, 'entities'), absolute: true }).catch(() =>
|
|
254
|
+
[] as string[]
|
|
255
|
+
),
|
|
256
|
+
glob(['*.json', '*.yaml', '*.yml'], { cwd: resolveAssetRoot(assets?.mcp, 'mcp'), absolute: true }).catch(() =>
|
|
257
|
+
[] as string[]
|
|
258
|
+
)
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
rulePaths,
|
|
263
|
+
skillPaths,
|
|
264
|
+
specPaths,
|
|
265
|
+
entityDocPaths,
|
|
266
|
+
entityJsonPaths,
|
|
267
|
+
mcpPaths
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const toOpenCodeOverlayEntries = (
|
|
272
|
+
kind: OpenCodeOverlayKind,
|
|
273
|
+
targetDir: 'agents' | 'commands' | 'modes' | 'plugins',
|
|
274
|
+
paths: string[]
|
|
275
|
+
): OpenCodeOverlayAssetEntry[] =>
|
|
276
|
+
paths.map((sourcePath) => ({
|
|
277
|
+
kind,
|
|
278
|
+
sourcePath,
|
|
279
|
+
entryName: basename(sourcePath, extname(sourcePath)),
|
|
280
|
+
targetSubpath: `${targetDir}/${basename(sourcePath)}`
|
|
281
|
+
}))
|
|
282
|
+
|
|
283
|
+
const scanInstanceOpenCodeOverlays = async (
|
|
284
|
+
instance: ResolvedPluginInstance
|
|
285
|
+
) => {
|
|
286
|
+
const opencodeRoot = resolve(instance.rootDir, 'opencode')
|
|
287
|
+
const [agentPaths, commandPaths, modePaths, nativePluginPaths] = await Promise.all([
|
|
288
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'agents'), absolute: true, onlyFiles: true }).catch(() =>
|
|
289
|
+
[] as string[]
|
|
290
|
+
),
|
|
291
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'commands'), absolute: true, onlyFiles: true }).catch(() =>
|
|
292
|
+
[] as string[]
|
|
293
|
+
),
|
|
294
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'modes'), absolute: true, onlyFiles: true }).catch(() =>
|
|
295
|
+
[] as string[]
|
|
296
|
+
),
|
|
297
|
+
glob(['**/*'], { cwd: resolve(opencodeRoot, 'plugins'), absolute: true, onlyFiles: true }).catch(() =>
|
|
298
|
+
[] as string[]
|
|
299
|
+
)
|
|
300
|
+
])
|
|
301
|
+
|
|
302
|
+
return [
|
|
303
|
+
...toOpenCodeOverlayEntries('agent', 'agents', agentPaths),
|
|
304
|
+
...toOpenCodeOverlayEntries('command', 'commands', commandPaths),
|
|
305
|
+
...toOpenCodeOverlayEntries('mode', 'modes', modePaths),
|
|
306
|
+
...toOpenCodeOverlayEntries('nativePlugin', 'plugins', nativePluginPaths)
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const assertNoDocumentConflicts = (
|
|
311
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>>
|
|
312
|
+
) => {
|
|
313
|
+
const seen = new Map<string, WorkspaceAsset>()
|
|
314
|
+
for (const asset of assets) {
|
|
315
|
+
const key = `${asset.kind}:${asset.displayName}`
|
|
316
|
+
const existing = seen.get(key)
|
|
317
|
+
if (existing != null) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Duplicate ${asset.kind} asset ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
seen.set(key, asset)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const assertNoMcpConflicts = (
|
|
327
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
|
|
328
|
+
) => {
|
|
329
|
+
const seen = new Map<string, WorkspaceAsset>()
|
|
330
|
+
for (const asset of assets) {
|
|
331
|
+
const existing = seen.get(asset.displayName)
|
|
332
|
+
if (existing != null) {
|
|
333
|
+
throw new Error(`Duplicate MCP server ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`)
|
|
334
|
+
}
|
|
335
|
+
seen.set(asset.displayName, asset)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function collectWorkspaceAssets(params: {
|
|
340
|
+
cwd: string
|
|
341
|
+
configs?: [Config?, Config?]
|
|
342
|
+
plugins?: PluginConfig
|
|
343
|
+
overlaySource?: string
|
|
344
|
+
useDefaultVibeForgeMcpServer?: boolean
|
|
345
|
+
}): Promise<{
|
|
346
|
+
assets: WorkspaceAsset[]
|
|
347
|
+
defaultExcludeMcpServers: string[]
|
|
348
|
+
defaultIncludeMcpServers: string[]
|
|
349
|
+
entities: Array<Extract<WorkspaceAsset, { kind: 'entity' }>>
|
|
350
|
+
hookPlugins: Extract<WorkspaceAsset, { kind: 'hookPlugin' }>[]
|
|
351
|
+
mcpServers: Record<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
|
|
352
|
+
opencodeOverlayAssets: Array<Extract<WorkspaceAsset, { kind: OpenCodeOverlayKind }>>
|
|
353
|
+
pluginConfigs: PluginConfig | undefined
|
|
354
|
+
pluginInstances: Awaited<ReturnType<typeof resolveConfiguredPluginInstances>>
|
|
355
|
+
rules: Array<Extract<WorkspaceAsset, { kind: 'rule' }>>
|
|
356
|
+
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
|
|
357
|
+
specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
|
|
358
|
+
}> {
|
|
359
|
+
const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
|
|
360
|
+
const pluginConfigs = params.plugins ?? mergePluginConfigs(config?.plugins, userConfig?.plugins)
|
|
361
|
+
const pluginInstances = await resolveConfiguredPluginInstances({
|
|
362
|
+
cwd: params.cwd,
|
|
363
|
+
plugins: pluginConfigs,
|
|
364
|
+
overlaySource: params.overlaySource
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const localScan = await scanWorkspaceDocuments(params.cwd)
|
|
368
|
+
const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
|
|
369
|
+
const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
|
|
370
|
+
const pluginOverlayScans = await Promise.all(
|
|
371
|
+
flattenedPluginInstances.map(instance => scanInstanceOpenCodeOverlays(instance))
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const assets: WorkspaceAsset[] = []
|
|
375
|
+
|
|
376
|
+
const pushDocumentAssets = async <TKind extends DocumentAssetKind>(
|
|
377
|
+
kind: TKind,
|
|
378
|
+
paths: string[],
|
|
379
|
+
origin: 'workspace' | 'plugin',
|
|
380
|
+
instance?: ResolvedPluginInstance,
|
|
381
|
+
parser?: (path: string) => Promise<any>
|
|
382
|
+
) => {
|
|
383
|
+
const definitions = await Promise.all(paths.map(path => (
|
|
384
|
+
parser != null ? parser(path) : parseFrontmatterDocument(path)
|
|
385
|
+
)))
|
|
386
|
+
assets.push(
|
|
387
|
+
...definitions.map(definition =>
|
|
388
|
+
createDocumentAsset({
|
|
389
|
+
cwd: params.cwd,
|
|
390
|
+
kind,
|
|
391
|
+
definition,
|
|
392
|
+
origin,
|
|
393
|
+
scope: instance?.scope,
|
|
394
|
+
instance
|
|
395
|
+
})
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await pushDocumentAssets('rule', localScan.rulePaths, 'workspace')
|
|
401
|
+
await pushDocumentAssets('skill', localScan.skillPaths, 'workspace')
|
|
402
|
+
await pushDocumentAssets('spec', localScan.specPaths, 'workspace')
|
|
403
|
+
await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace')
|
|
404
|
+
await pushDocumentAssets('entity', localScan.entityJsonPaths, 'workspace', undefined, parseEntityIndexJson)
|
|
405
|
+
|
|
406
|
+
for (let index = 0; index < flattenedPluginInstances.length; index++) {
|
|
407
|
+
const instance = flattenedPluginInstances[index]
|
|
408
|
+
const scan = pluginScans[index]
|
|
409
|
+
await pushDocumentAssets('rule', scan.rulePaths, 'plugin', instance)
|
|
410
|
+
await pushDocumentAssets('skill', scan.skillPaths, 'plugin', instance)
|
|
411
|
+
await pushDocumentAssets('spec', scan.specPaths, 'plugin', instance)
|
|
412
|
+
await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance)
|
|
413
|
+
await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const mcpAssets = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
|
|
417
|
+
const addMcpAsset = (
|
|
418
|
+
asset: Extract<WorkspaceAsset, { kind: 'mcpServer' }>,
|
|
419
|
+
options?: { overwrite?: boolean }
|
|
420
|
+
) => {
|
|
421
|
+
const existing = mcpAssets.get(asset.displayName)
|
|
422
|
+
if (existing != null && options?.overwrite !== true) {
|
|
423
|
+
throw new Error(`Duplicate MCP server ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`)
|
|
424
|
+
}
|
|
425
|
+
mcpAssets.set(asset.displayName, asset)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (params.useDefaultVibeForgeMcpServer !== false) {
|
|
429
|
+
const defaultVibeForgeMcpServer = resolveDefaultVibeForgeMcpServerConfig()
|
|
430
|
+
if (defaultVibeForgeMcpServer != null) {
|
|
431
|
+
addMcpAsset(createMcpAsset({
|
|
432
|
+
cwd: params.cwd,
|
|
433
|
+
name: DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
434
|
+
config: defaultVibeForgeMcpServer,
|
|
435
|
+
origin: 'workspace',
|
|
436
|
+
sourcePath: resolve(params.cwd, '.ai')
|
|
437
|
+
}))
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const [name, configValue] of Object.entries(config?.mcpServers ?? {})) {
|
|
442
|
+
if (configValue.enabled === false) continue
|
|
443
|
+
const { enabled: _enabled, ...nextConfig } = configValue
|
|
444
|
+
addMcpAsset(
|
|
445
|
+
createMcpAsset({
|
|
446
|
+
cwd: params.cwd,
|
|
447
|
+
name,
|
|
448
|
+
config: nextConfig as NonNullable<Config['mcpServers']>[string],
|
|
449
|
+
origin: 'workspace',
|
|
450
|
+
sourcePath: resolve(params.cwd, '.ai.config.json')
|
|
451
|
+
}),
|
|
452
|
+
{ overwrite: true }
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const [name, configValue] of Object.entries(userConfig?.mcpServers ?? {})) {
|
|
457
|
+
if (configValue.enabled === false) continue
|
|
458
|
+
const { enabled: _enabled, ...nextConfig } = configValue
|
|
459
|
+
addMcpAsset(
|
|
460
|
+
createMcpAsset({
|
|
461
|
+
cwd: params.cwd,
|
|
462
|
+
name,
|
|
463
|
+
config: nextConfig as NonNullable<Config['mcpServers']>[string],
|
|
464
|
+
origin: 'workspace',
|
|
465
|
+
sourcePath: resolve(params.cwd, '.ai.dev.config.json')
|
|
466
|
+
}),
|
|
467
|
+
{ overwrite: true }
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (let index = 0; index < flattenedPluginInstances.length; index++) {
|
|
472
|
+
const instance = flattenedPluginInstances[index]
|
|
473
|
+
const scan = pluginScans[index]
|
|
474
|
+
for (const path of scan.mcpPaths) {
|
|
475
|
+
const parsed = await parseStructuredMcpFile(path)
|
|
476
|
+
if (!isRecord(parsed)) continue
|
|
477
|
+
const fileName = basename(path, extname(path))
|
|
478
|
+
const name = typeof parsed.name === 'string' && parsed.name.trim() !== ''
|
|
479
|
+
? parsed.name.trim()
|
|
480
|
+
: fileName
|
|
481
|
+
const { name: _name, enabled, ...configValue } = parsed
|
|
482
|
+
if (enabled === false) continue
|
|
483
|
+
addMcpAsset(createMcpAsset({
|
|
484
|
+
cwd: params.cwd,
|
|
485
|
+
name,
|
|
486
|
+
config: configValue as NonNullable<Config['mcpServers']>[string],
|
|
487
|
+
origin: 'plugin',
|
|
488
|
+
scope: instance.scope,
|
|
489
|
+
sourcePath: path,
|
|
490
|
+
instance
|
|
491
|
+
}))
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const hookPlugins = flattenedPluginInstances
|
|
496
|
+
.filter(instance =>
|
|
497
|
+
instance.packageId != null && resolvePluginHooksEntryPath(params.cwd, instance.packageId) != null
|
|
498
|
+
)
|
|
499
|
+
.map(instance => createHookPluginAsset(instance))
|
|
500
|
+
assets.push(...hookPlugins)
|
|
501
|
+
|
|
502
|
+
const opencodeOverlayAssets = flattenedPluginInstances.flatMap((instance, index) => (
|
|
503
|
+
pluginOverlayScans[index].map((entry) =>
|
|
504
|
+
createOpenCodeOverlayAsset({
|
|
505
|
+
cwd: params.cwd,
|
|
506
|
+
kind: entry.kind,
|
|
507
|
+
sourcePath: entry.sourcePath,
|
|
508
|
+
entryName: entry.entryName,
|
|
509
|
+
targetSubpath: entry.targetSubpath,
|
|
510
|
+
instance
|
|
511
|
+
})
|
|
512
|
+
)
|
|
513
|
+
))
|
|
514
|
+
assets.push(...opencodeOverlayAssets)
|
|
515
|
+
|
|
516
|
+
assets.push(...mcpAssets.values())
|
|
517
|
+
|
|
518
|
+
const rules = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'rule' }> => asset.kind === 'rule')
|
|
519
|
+
const specs = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'spec' }> => asset.kind === 'spec')
|
|
520
|
+
const entities = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'entity' }> =>
|
|
521
|
+
asset.kind === 'entity'
|
|
522
|
+
)
|
|
523
|
+
const skills = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'skill' }> => asset.kind === 'skill')
|
|
524
|
+
|
|
525
|
+
assertNoDocumentConflicts([...rules, ...specs, ...entities, ...skills])
|
|
526
|
+
assertNoMcpConflicts(Array.from(mcpAssets.values()))
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
assets,
|
|
530
|
+
defaultExcludeMcpServers: [
|
|
531
|
+
...(config?.defaultExcludeMcpServers ?? []),
|
|
532
|
+
...(userConfig?.defaultExcludeMcpServers ?? [])
|
|
533
|
+
],
|
|
534
|
+
defaultIncludeMcpServers: [
|
|
535
|
+
...(config?.defaultIncludeMcpServers ?? []),
|
|
536
|
+
...(userConfig?.defaultIncludeMcpServers ?? [])
|
|
537
|
+
],
|
|
538
|
+
entities,
|
|
539
|
+
hookPlugins,
|
|
540
|
+
mcpServers: Object.fromEntries(Array.from(mcpAssets.values()).map(asset => [asset.displayName, asset])),
|
|
541
|
+
opencodeOverlayAssets,
|
|
542
|
+
pluginConfigs,
|
|
543
|
+
pluginInstances,
|
|
544
|
+
rules,
|
|
545
|
+
skills,
|
|
546
|
+
specs
|
|
547
|
+
}
|
|
548
|
+
}
|