@vibe-forge/workspace-assets 0.8.4 → 0.9.1-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 +355 -360
- package/__tests__/adapter-asset-plan.spec.ts +40 -66
- package/__tests__/bundle.spec.ts +44 -168
- package/__tests__/prompt-selection.spec.ts +14 -358
- package/__tests__/snapshot.ts +127 -75
- package/__tests__/test-helpers.ts +0 -11
- package/__tests__/workspace-assets.snapshot.spec.ts +103 -72
- package/package.json +10 -10
- package/src/adapter-asset-plan.ts +174 -0
- package/src/bundle.ts +178 -0
- package/src/document-assets.ts +191 -0
- package/src/helpers.ts +35 -0
- package/src/index.ts +3 -1368
- package/src/internal-types.ts +39 -1
- package/src/plugin-assets.ts +175 -0
- package/src/prompt-selection.ts +151 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdapterAssetPlan,
|
|
5
|
+
AssetDiagnostic,
|
|
6
|
+
WorkspaceAssetAdapter,
|
|
7
|
+
WorkspaceAssetBundle,
|
|
8
|
+
WorkspaceMcpSelection,
|
|
9
|
+
WorkspaceSkillSelection
|
|
10
|
+
} from '@vibe-forge/types'
|
|
11
|
+
|
|
12
|
+
import { filterSkillAssets } from './document-assets'
|
|
13
|
+
import { isOpenCodeOverlayAsset } from './internal-types'
|
|
14
|
+
|
|
15
|
+
const resolveMcpServerSelection = (
|
|
16
|
+
bundle: WorkspaceAssetBundle,
|
|
17
|
+
selection: WorkspaceMcpSelection | undefined
|
|
18
|
+
) => {
|
|
19
|
+
const include = selection?.include ?? (
|
|
20
|
+
bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined
|
|
21
|
+
)
|
|
22
|
+
const exclude = selection?.exclude ?? (
|
|
23
|
+
bundle.defaultExcludeMcpServers.length > 0 ? bundle.defaultExcludeMcpServers : undefined
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
include,
|
|
28
|
+
exclude
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildAdapterAssetPlan(params: {
|
|
33
|
+
adapter: WorkspaceAssetAdapter
|
|
34
|
+
bundle: WorkspaceAssetBundle
|
|
35
|
+
options: {
|
|
36
|
+
mcpServers?: WorkspaceMcpSelection
|
|
37
|
+
skills?: WorkspaceSkillSelection
|
|
38
|
+
promptAssetIds?: string[]
|
|
39
|
+
}
|
|
40
|
+
}): AdapterAssetPlan {
|
|
41
|
+
const diagnostics: AssetDiagnostic[] = []
|
|
42
|
+
const promptAssetIdSet = new Set(params.options.promptAssetIds ?? [])
|
|
43
|
+
const mcpSelection = resolveMcpServerSelection(params.bundle, params.options.mcpServers)
|
|
44
|
+
const selectedMcpServerNames = Object.keys(params.bundle.mcpServers).filter((name) => {
|
|
45
|
+
if (mcpSelection.include != null && !mcpSelection.include.includes(name)) return false
|
|
46
|
+
if (mcpSelection.exclude?.includes(name)) return false
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
const mcpServers = Object.fromEntries(
|
|
50
|
+
selectedMcpServerNames.map((name) => [name, params.bundle.mcpServers[name].payload.config])
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for (const assetId of promptAssetIdSet) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
assetId,
|
|
56
|
+
adapter: params.adapter,
|
|
57
|
+
status: 'prompt',
|
|
58
|
+
reason: 'Mapped into the generated system prompt.'
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const name of selectedMcpServerNames) {
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
assetId: params.bundle.mcpServers[name].id,
|
|
65
|
+
adapter: params.adapter,
|
|
66
|
+
status: params.adapter === 'claude-code' ? 'native' : 'translated',
|
|
67
|
+
reason: params.adapter === 'claude-code'
|
|
68
|
+
? 'Mapped into native MCP settings.'
|
|
69
|
+
: 'Translated into adapter-specific MCP configuration.'
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const hookPlugin of params.bundle.hookPlugins) {
|
|
74
|
+
const nativeHookReason = params.adapter === 'claude-code'
|
|
75
|
+
? 'Mapped into the isolated Claude Code native hooks bridge under .ai/.mock/.claude/settings.json.'
|
|
76
|
+
: params.adapter === 'codex'
|
|
77
|
+
? 'Mapped into the isolated Codex native hooks bridge for SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop.'
|
|
78
|
+
: 'Mapped into the isolated OpenCode native hook plugin bridge under .ai/.mock/.config/opencode/plugins.'
|
|
79
|
+
diagnostics.push({
|
|
80
|
+
assetId: hookPlugin.id,
|
|
81
|
+
adapter: params.adapter,
|
|
82
|
+
status: 'native',
|
|
83
|
+
reason: nativeHookReason
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const overlays: AdapterAssetPlan['overlays'] = []
|
|
88
|
+
if (params.adapter === 'opencode') {
|
|
89
|
+
const skillAssets = filterSkillAssets(params.bundle.skills, params.options.skills)
|
|
90
|
+
for (const skillAsset of skillAssets) {
|
|
91
|
+
overlays.push({
|
|
92
|
+
assetId: skillAsset.id,
|
|
93
|
+
kind: 'skill',
|
|
94
|
+
sourcePath: dirname(skillAsset.payload.definition.path),
|
|
95
|
+
targetPath: `skills/${basename(dirname(skillAsset.payload.definition.path))}`
|
|
96
|
+
})
|
|
97
|
+
diagnostics.push({
|
|
98
|
+
assetId: skillAsset.id,
|
|
99
|
+
adapter: 'opencode',
|
|
100
|
+
status: 'native',
|
|
101
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native skill.'
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const asset of params.bundle.assets) {
|
|
106
|
+
if (!isOpenCodeOverlayAsset(asset)) continue
|
|
107
|
+
if (!asset.targets.includes('opencode')) continue
|
|
108
|
+
|
|
109
|
+
overlays.push({
|
|
110
|
+
assetId: asset.id,
|
|
111
|
+
kind: asset.kind,
|
|
112
|
+
sourcePath: asset.payload.sourcePath,
|
|
113
|
+
targetPath: asset.payload.targetSubpath
|
|
114
|
+
})
|
|
115
|
+
diagnostics.push({
|
|
116
|
+
assetId: asset.id,
|
|
117
|
+
adapter: 'opencode',
|
|
118
|
+
status: 'native',
|
|
119
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (params.adapter !== 'claude-code') {
|
|
125
|
+
for (const asset of params.bundle.assets) {
|
|
126
|
+
if (asset.kind !== 'nativePlugin' || !asset.enabled || !asset.targets.includes('claude-code')) continue
|
|
127
|
+
diagnostics.push({
|
|
128
|
+
assetId: asset.id,
|
|
129
|
+
adapter: params.adapter,
|
|
130
|
+
status: 'skipped',
|
|
131
|
+
reason: 'Claude marketplace plugin settings do not have a native mapping for this adapter.'
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (params.adapter === 'codex') {
|
|
137
|
+
for (const asset of params.bundle.assets) {
|
|
138
|
+
if (!['nativePlugin', 'agent', 'command', 'mode'].includes(asset.kind)) continue
|
|
139
|
+
if (asset.targets.includes('codex')) continue
|
|
140
|
+
if (asset.kind === 'nativePlugin' && asset.targets.includes('claude-code')) continue
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
assetId: asset.id,
|
|
143
|
+
adapter: 'codex',
|
|
144
|
+
status: 'skipped',
|
|
145
|
+
reason: 'No stable native Codex mapping exists for this asset kind in V1.'
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
adapter: params.adapter,
|
|
152
|
+
diagnostics,
|
|
153
|
+
mcpServers,
|
|
154
|
+
overlays,
|
|
155
|
+
native: params.adapter === 'claude-code'
|
|
156
|
+
? {
|
|
157
|
+
enabledPlugins: params.bundle.enabledPlugins,
|
|
158
|
+
extraKnownMarketplaces: params.bundle.extraKnownMarketplaces
|
|
159
|
+
}
|
|
160
|
+
: params.adapter === 'codex' && params.bundle.hookPlugins.length > 0
|
|
161
|
+
? {
|
|
162
|
+
codexHooks: {
|
|
163
|
+
supportedEvents: [
|
|
164
|
+
'SessionStart',
|
|
165
|
+
'UserPromptSubmit',
|
|
166
|
+
'PreToolUse',
|
|
167
|
+
'PostToolUse',
|
|
168
|
+
'Stop'
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
: {}
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
5
|
+
buildConfigJsonVariables,
|
|
6
|
+
loadConfig,
|
|
7
|
+
resolveDefaultVibeForgeMcpServerConfig
|
|
8
|
+
} from '@vibe-forge/config'
|
|
9
|
+
import { DefinitionLoader } from '@vibe-forge/definition-loader'
|
|
10
|
+
import type { Config, WorkspaceAsset, WorkspaceAssetBundle } from '@vibe-forge/types'
|
|
11
|
+
import { resolveDocumentName, resolveSpecIdentifier } from '@vibe-forge/utils'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createDocumentAsset,
|
|
15
|
+
dedupeDocumentAssets,
|
|
16
|
+
dedupeDocumentAssetsByIdentifier,
|
|
17
|
+
resolveRuleIdentifier,
|
|
18
|
+
resolveSkillIdentifier
|
|
19
|
+
} from './document-assets'
|
|
20
|
+
import { mergeRecord, uniqueValues } from './helpers'
|
|
21
|
+
import {
|
|
22
|
+
createClaudeNativePluginAssets,
|
|
23
|
+
createHookPluginAssets,
|
|
24
|
+
loadOpenCodeOverlayAssets,
|
|
25
|
+
loadPluginMcpAssets
|
|
26
|
+
} from './plugin-assets'
|
|
27
|
+
|
|
28
|
+
const readConfigForWorkspace = async (cwd: string) => {
|
|
29
|
+
return loadConfig({
|
|
30
|
+
cwd,
|
|
31
|
+
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function resolveWorkspaceAssetBundle(params: {
|
|
36
|
+
cwd: string
|
|
37
|
+
configs?: [Config?, Config?]
|
|
38
|
+
useDefaultVibeForgeMcpServer?: boolean
|
|
39
|
+
}): Promise<WorkspaceAssetBundle> {
|
|
40
|
+
const [config, userConfig] = params.configs ?? await readConfigForWorkspace(params.cwd)
|
|
41
|
+
const enabledPlugins = mergeRecord(config?.enabledPlugins, userConfig?.enabledPlugins)
|
|
42
|
+
const extraKnownMarketplaces = mergeRecord(config?.extraKnownMarketplaces, userConfig?.extraKnownMarketplaces)
|
|
43
|
+
const loader = new DefinitionLoader(params.cwd)
|
|
44
|
+
|
|
45
|
+
const [
|
|
46
|
+
rawRules,
|
|
47
|
+
rawSpecs,
|
|
48
|
+
rawEntities,
|
|
49
|
+
rawSkills,
|
|
50
|
+
pluginMcpAssets,
|
|
51
|
+
openCodeOverlayAssets
|
|
52
|
+
] = await Promise.all([
|
|
53
|
+
loader.loadDefaultRules(),
|
|
54
|
+
loader.loadDefaultSpecs(),
|
|
55
|
+
loader.loadDefaultEntities(),
|
|
56
|
+
loader.loadDefaultSkills(),
|
|
57
|
+
loadPluginMcpAssets(params.cwd, enabledPlugins),
|
|
58
|
+
loadOpenCodeOverlayAssets(params.cwd, enabledPlugins)
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
const assets: WorkspaceAsset[] = []
|
|
62
|
+
|
|
63
|
+
const rules = dedupeDocumentAssetsByIdentifier(
|
|
64
|
+
dedupeDocumentAssets(
|
|
65
|
+
rawRules.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'rule', definition })),
|
|
66
|
+
enabledPlugins
|
|
67
|
+
),
|
|
68
|
+
asset => resolveRuleIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
69
|
+
)
|
|
70
|
+
const specs = dedupeDocumentAssetsByIdentifier(
|
|
71
|
+
dedupeDocumentAssets(
|
|
72
|
+
rawSpecs.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'spec', definition })),
|
|
73
|
+
enabledPlugins
|
|
74
|
+
),
|
|
75
|
+
asset => resolveSpecIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
76
|
+
)
|
|
77
|
+
const entities = dedupeDocumentAssetsByIdentifier(
|
|
78
|
+
dedupeDocumentAssets(
|
|
79
|
+
rawEntities.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'entity', definition })),
|
|
80
|
+
enabledPlugins
|
|
81
|
+
),
|
|
82
|
+
asset =>
|
|
83
|
+
resolveDocumentName(
|
|
84
|
+
asset.payload.definition.path,
|
|
85
|
+
asset.payload.definition.attributes.name,
|
|
86
|
+
['readme.md', 'index.json']
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
const skills = dedupeDocumentAssetsByIdentifier(
|
|
90
|
+
dedupeDocumentAssets(
|
|
91
|
+
rawSkills.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'skill', definition })),
|
|
92
|
+
enabledPlugins
|
|
93
|
+
),
|
|
94
|
+
asset => resolveSkillIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assets.push(...rules, ...specs, ...entities, ...skills)
|
|
98
|
+
|
|
99
|
+
const mcpServers = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
|
|
100
|
+
if (params.useDefaultVibeForgeMcpServer !== false) {
|
|
101
|
+
const defaultVibeForgeMcpServer = resolveDefaultVibeForgeMcpServerConfig()
|
|
102
|
+
if (defaultVibeForgeMcpServer != null) {
|
|
103
|
+
mcpServers.set(DEFAULT_VIBE_FORGE_MCP_SERVER_NAME, {
|
|
104
|
+
id: `mcpServer:fallback:${DEFAULT_VIBE_FORGE_MCP_SERVER_NAME}`,
|
|
105
|
+
kind: 'mcpServer',
|
|
106
|
+
origin: 'fallback',
|
|
107
|
+
scope: 'adapter',
|
|
108
|
+
enabled: true,
|
|
109
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
110
|
+
payload: {
|
|
111
|
+
name: DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
112
|
+
config: defaultVibeForgeMcpServer
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const userMcpServers = userConfig?.mcpServers ?? {}
|
|
118
|
+
for (const [name, serverConfig] of Object.entries(userMcpServers)) {
|
|
119
|
+
mcpServers.set(name, {
|
|
120
|
+
id: `mcpServer:user:${name}`,
|
|
121
|
+
kind: 'mcpServer',
|
|
122
|
+
origin: 'config',
|
|
123
|
+
scope: 'user',
|
|
124
|
+
enabled: true,
|
|
125
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
126
|
+
payload: {
|
|
127
|
+
name,
|
|
128
|
+
config: serverConfig
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
for (const asset of pluginMcpAssets) {
|
|
133
|
+
mcpServers.set(asset.payload.name, asset)
|
|
134
|
+
}
|
|
135
|
+
for (const [name, serverConfig] of Object.entries(config?.mcpServers ?? {})) {
|
|
136
|
+
mcpServers.set(name, {
|
|
137
|
+
id: `mcpServer:project:${name}`,
|
|
138
|
+
kind: 'mcpServer',
|
|
139
|
+
origin: 'config',
|
|
140
|
+
scope: 'project',
|
|
141
|
+
enabled: true,
|
|
142
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
143
|
+
payload: {
|
|
144
|
+
name,
|
|
145
|
+
config: serverConfig
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
assets.push(...mcpServers.values())
|
|
150
|
+
|
|
151
|
+
const hookPlugins = [
|
|
152
|
+
...createHookPluginAssets(userConfig?.plugins, enabledPlugins, 'user'),
|
|
153
|
+
...createHookPluginAssets(config?.plugins, enabledPlugins, 'project')
|
|
154
|
+
]
|
|
155
|
+
const claudeNativePlugins = createClaudeNativePluginAssets(enabledPlugins)
|
|
156
|
+
assets.push(...hookPlugins, ...claudeNativePlugins, ...openCodeOverlayAssets)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
cwd: params.cwd,
|
|
160
|
+
assets,
|
|
161
|
+
rules,
|
|
162
|
+
specs,
|
|
163
|
+
entities,
|
|
164
|
+
skills,
|
|
165
|
+
mcpServers: Object.fromEntries(mcpServers.entries()),
|
|
166
|
+
hookPlugins,
|
|
167
|
+
enabledPlugins,
|
|
168
|
+
extraKnownMarketplaces,
|
|
169
|
+
defaultIncludeMcpServers: uniqueValues([
|
|
170
|
+
...(config?.defaultIncludeMcpServers ?? []),
|
|
171
|
+
...(userConfig?.defaultIncludeMcpServers ?? [])
|
|
172
|
+
]),
|
|
173
|
+
defaultExcludeMcpServers: uniqueValues([
|
|
174
|
+
...(config?.defaultExcludeMcpServers ?? []),
|
|
175
|
+
...(userConfig?.defaultExcludeMcpServers ?? [])
|
|
176
|
+
])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
RuleReference,
|
|
5
|
+
SkillSelection,
|
|
6
|
+
WorkspaceAsset,
|
|
7
|
+
WorkspaceAssetAdapter,
|
|
8
|
+
WorkspaceAssetBundle,
|
|
9
|
+
WorkspaceAssetKind,
|
|
10
|
+
WorkspaceSkillSelection
|
|
11
|
+
} from '@vibe-forge/types'
|
|
12
|
+
import { normalizePath, resolveDocumentName, resolveRelativePath, resolveSpecIdentifier } from '@vibe-forge/utils'
|
|
13
|
+
import { glob } from 'fast-glob'
|
|
14
|
+
|
|
15
|
+
import { assetOriginPriority, isPluginEnabled, resolvePluginIdFromPath, toAssetScope } from './helpers'
|
|
16
|
+
import type { WorkspaceDocumentAsset, WorkspaceDocumentPayload } from './internal-types'
|
|
17
|
+
|
|
18
|
+
const isLocalRuleReference = (
|
|
19
|
+
rule: RuleReference
|
|
20
|
+
): rule is Extract<RuleReference, { path: string }> => (
|
|
21
|
+
rule != null &&
|
|
22
|
+
typeof rule === 'object' &&
|
|
23
|
+
'path' in rule &&
|
|
24
|
+
typeof rule.path === 'string'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export const createDocumentAsset = <
|
|
28
|
+
TKind extends Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>,
|
|
29
|
+
TDefinition,
|
|
30
|
+
>(
|
|
31
|
+
params: {
|
|
32
|
+
cwd: string
|
|
33
|
+
kind: TKind
|
|
34
|
+
definition: TDefinition & { path: string }
|
|
35
|
+
targets?: WorkspaceAssetAdapter[]
|
|
36
|
+
}
|
|
37
|
+
): Extract<WorkspaceAsset, { kind: TKind }> => {
|
|
38
|
+
const pluginId = resolvePluginIdFromPath(params.cwd, params.definition.path)
|
|
39
|
+
const origin: WorkspaceAsset['origin'] = pluginId == null ? 'project' : 'plugin'
|
|
40
|
+
return {
|
|
41
|
+
id: `${params.kind}:${resolveRelativePath(params.cwd, params.definition.path)}`,
|
|
42
|
+
kind: params.kind,
|
|
43
|
+
pluginId,
|
|
44
|
+
origin,
|
|
45
|
+
scope: toAssetScope(origin),
|
|
46
|
+
enabled: true,
|
|
47
|
+
targets: params.targets ?? ['claude-code', 'codex', 'opencode'],
|
|
48
|
+
payload: {
|
|
49
|
+
definition: params.definition as any,
|
|
50
|
+
sourcePath: params.definition.path
|
|
51
|
+
}
|
|
52
|
+
} as Extract<WorkspaceAsset, { kind: TKind }>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const dedupeDocumentAssets = <
|
|
56
|
+
TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
|
|
57
|
+
>(
|
|
58
|
+
assets: TAsset[],
|
|
59
|
+
enabledPlugins: Record<string, boolean>
|
|
60
|
+
) => assets.filter((asset) => isPluginEnabled(enabledPlugins, asset.pluginId))
|
|
61
|
+
|
|
62
|
+
const compareDocumentAssetPriority = (
|
|
63
|
+
left: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
|
|
64
|
+
right: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>
|
|
65
|
+
) => {
|
|
66
|
+
const originDiff = assetOriginPriority[left.origin] - assetOriginPriority[right.origin]
|
|
67
|
+
if (originDiff !== 0) return originDiff
|
|
68
|
+
return left.payload.definition.path.localeCompare(right.payload.definition.path)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const dedupeDocumentAssetsByIdentifier = <
|
|
72
|
+
TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
|
|
73
|
+
>(
|
|
74
|
+
assets: TAsset[],
|
|
75
|
+
resolveIdentifier: (asset: TAsset) => string
|
|
76
|
+
) => {
|
|
77
|
+
const selected = new Map<string, TAsset>()
|
|
78
|
+
|
|
79
|
+
for (const asset of [...assets].sort(compareDocumentAssetPriority)) {
|
|
80
|
+
const identifier = resolveIdentifier(asset)
|
|
81
|
+
if (!selected.has(identifier)) selected.set(identifier, asset)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Array.from(selected.values()).sort(compareDocumentAssetPriority)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const resolveRuleIdentifier = (
|
|
88
|
+
path: string,
|
|
89
|
+
explicitName?: string
|
|
90
|
+
) => resolveDocumentName(path, explicitName)
|
|
91
|
+
|
|
92
|
+
export const resolveSkillIdentifier = (
|
|
93
|
+
path: string,
|
|
94
|
+
explicitName?: string
|
|
95
|
+
) => resolveDocumentName(path, explicitName, ['skill.md'])
|
|
96
|
+
|
|
97
|
+
export const pickSpecAsset = (
|
|
98
|
+
bundle: WorkspaceAssetBundle,
|
|
99
|
+
name: string
|
|
100
|
+
): Extract<WorkspaceAsset, { kind: 'spec' }> | undefined => {
|
|
101
|
+
const assets = bundle.specs.filter((asset) => {
|
|
102
|
+
const definition = asset.payload.definition
|
|
103
|
+
return resolveSpecIdentifier(definition.path, definition.attributes.name) === name
|
|
104
|
+
})
|
|
105
|
+
return assets.find(asset => asset.origin === 'project') ?? assets[0]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const pickEntityAsset = (
|
|
109
|
+
bundle: WorkspaceAssetBundle,
|
|
110
|
+
name: string
|
|
111
|
+
): Extract<WorkspaceAsset, { kind: 'entity' }> | undefined => {
|
|
112
|
+
const assets = bundle.entities.filter((asset) => {
|
|
113
|
+
const definition = asset.payload.definition
|
|
114
|
+
const identifier = resolveDocumentName(definition.path, definition.attributes.name, ['readme.md', 'index.json'])
|
|
115
|
+
return identifier === name
|
|
116
|
+
})
|
|
117
|
+
return assets.find(asset => asset.origin === 'project') ?? assets[0]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const filterSkillAssets = (
|
|
121
|
+
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
|
|
122
|
+
selection?: WorkspaceSkillSelection
|
|
123
|
+
): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
|
|
124
|
+
if (selection == null) return skills
|
|
125
|
+
|
|
126
|
+
const include = selection.include != null && selection.include.length > 0
|
|
127
|
+
? new Set(selection.include)
|
|
128
|
+
: undefined
|
|
129
|
+
const exclude = new Set(selection.exclude ?? [])
|
|
130
|
+
|
|
131
|
+
return skills.filter((skill) => {
|
|
132
|
+
const name = basename(dirname(skill.payload.definition.path))
|
|
133
|
+
return (include == null || include.has(name)) && !exclude.has(name)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const dedupeSkillAssets = (
|
|
138
|
+
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
|
|
139
|
+
): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
|
|
140
|
+
const seen = new Set<string>()
|
|
141
|
+
return skills.filter((skill) => {
|
|
142
|
+
if (seen.has(skill.payload.definition.path)) return false
|
|
143
|
+
seen.add(skill.payload.definition.path)
|
|
144
|
+
return true
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const resolveRulePatterns = (rules: RuleReference[]) => (
|
|
149
|
+
rules.flatMap((rule) => {
|
|
150
|
+
if (typeof rule === 'string') return [rule]
|
|
151
|
+
if (isLocalRuleReference(rule)) return [rule.path]
|
|
152
|
+
return []
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
export const resolveIncludedSkillNames = (selection: string[] | SkillSelection) => (
|
|
157
|
+
Array.isArray(selection)
|
|
158
|
+
? selection
|
|
159
|
+
: selection.type === 'include'
|
|
160
|
+
? selection.list
|
|
161
|
+
: []
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
export const resolveExcludedSkillNames = (selection: string[] | SkillSelection) => (
|
|
165
|
+
Array.isArray(selection)
|
|
166
|
+
? []
|
|
167
|
+
: selection.type === 'exclude'
|
|
168
|
+
? selection.list
|
|
169
|
+
: []
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
export const resolveSelectedRuleAssets = async (
|
|
173
|
+
bundle: WorkspaceAssetBundle,
|
|
174
|
+
patterns: string[]
|
|
175
|
+
): Promise<Array<Extract<WorkspaceAsset, { kind: 'rule' }>>> => {
|
|
176
|
+
const matchedPaths = new Set(
|
|
177
|
+
(await glob(patterns, { cwd: bundle.cwd, absolute: true }))
|
|
178
|
+
.map(normalizePath)
|
|
179
|
+
)
|
|
180
|
+
return bundle.rules.filter((asset) => matchedPaths.has(normalizePath(asset.payload.definition.path)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const toDocumentDefinitions = <TDefinition>(
|
|
184
|
+
assets: Array<WorkspaceDocumentAsset<TDefinition>>
|
|
185
|
+
) => assets.map(asset => asset.payload.definition)
|
|
186
|
+
|
|
187
|
+
export const toPromptAssetIds = (assets: Array<{ id: string }>) => (
|
|
188
|
+
Array.from(new Set(assets.map(asset => asset.id)))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
export type { WorkspaceDocumentAsset, WorkspaceDocumentPayload }
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { WorkspaceAsset } from '@vibe-forge/types'
|
|
2
|
+
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
3
|
+
|
|
4
|
+
export const resolvePluginIdFromPath = (cwd: string, path: string) => {
|
|
5
|
+
const relativePath = resolveRelativePath(cwd, path)
|
|
6
|
+
const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\//)
|
|
7
|
+
return match?.[1]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const isPluginEnabled = (
|
|
11
|
+
enabledPlugins: Record<string, boolean>,
|
|
12
|
+
pluginId?: string
|
|
13
|
+
) => pluginId == null || enabledPlugins[pluginId] !== false
|
|
14
|
+
|
|
15
|
+
export const mergeRecord = <T>(left?: Record<string, T>, right?: Record<string, T>) => ({
|
|
16
|
+
...(left ?? {}),
|
|
17
|
+
...(right ?? {})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const uniqueValues = (values: string[]) => Array.from(new Set(values.filter(Boolean)))
|
|
21
|
+
|
|
22
|
+
export const assetOriginPriority: Record<WorkspaceAsset['origin'], number> = {
|
|
23
|
+
project: 0,
|
|
24
|
+
plugin: 1,
|
|
25
|
+
config: 2,
|
|
26
|
+
fallback: 3
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const toAssetScope = (origin: WorkspaceAsset['origin']): WorkspaceAsset['scope'] => (
|
|
30
|
+
origin === 'config'
|
|
31
|
+
? 'project'
|
|
32
|
+
: origin === 'fallback'
|
|
33
|
+
? 'adapter'
|
|
34
|
+
: 'workspace'
|
|
35
|
+
)
|