@vibe-forge/workspace-assets 2.0.0 → 2.0.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/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +55 -0
- package/__tests__/adapter-asset-plan.spec.ts +220 -6
- package/__tests__/bundle.spec.ts +677 -2
- package/__tests__/prompt-selection.spec.ts +307 -0
- package/__tests__/skill-dependencies-cli.spec.ts +175 -0
- package/__tests__/snapshot.ts +1 -0
- package/__tests__/test-helpers.ts +9 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +2 -2
- package/package.json +5 -5
- package/src/adapter-asset-plan.ts +42 -66
- package/src/asset-source.ts +13 -0
- package/src/bundle-internal.ts +242 -22
- package/src/bundle.ts +4 -0
- package/src/configured-skills.ts +99 -0
- package/src/home-bridge.ts +1 -0
- package/src/index.ts +3 -0
- package/src/prompt-selection.ts +44 -19
- package/src/selection-internal.ts +335 -1
- package/src/skill-dependencies.ts +361 -0
- package/src/skills-cli-dependency.ts +208 -0
- package/src/workspace-config.ts +132 -0
- package/src/workspace-prompt.ts +29 -0
- package/src/workspaces.ts +188 -0
|
@@ -11,9 +11,10 @@ import type {
|
|
|
11
11
|
} from '@vibe-forge/types'
|
|
12
12
|
|
|
13
13
|
import { resolveNativeSkillDiagnosticReason, supportsNativeProjectSkills } from './adapter-capabilities'
|
|
14
|
-
import {
|
|
14
|
+
import { resolveWorkspaceAssetSource } from './asset-source'
|
|
15
|
+
import { resolveSelectedMcpNames, resolveSelectedSkillAssetsWithDependencies } from './selection-internal'
|
|
15
16
|
|
|
16
|
-
export function buildAdapterAssetPlan(params: {
|
|
17
|
+
export async function buildAdapterAssetPlan(params: {
|
|
17
18
|
adapter: WorkspaceAssetAdapter
|
|
18
19
|
bundle: WorkspaceAssetBundle
|
|
19
20
|
options: {
|
|
@@ -21,17 +22,24 @@ export function buildAdapterAssetPlan(params: {
|
|
|
21
22
|
skills?: WorkspaceSkillSelection
|
|
22
23
|
promptAssetIds?: string[]
|
|
23
24
|
}
|
|
24
|
-
}): AdapterAssetPlan {
|
|
25
|
+
}): Promise<AdapterAssetPlan> {
|
|
25
26
|
const diagnostics: AssetDiagnostic[] = []
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const pushDiagnostic = (
|
|
28
|
+
asset: Parameters<typeof resolveWorkspaceAssetSource>[0] & {
|
|
29
|
+
id: string
|
|
30
|
+
packageId?: string
|
|
31
|
+
scope?: string
|
|
32
|
+
instancePath?: string
|
|
33
|
+
taskOverlaySource?: string
|
|
34
|
+
},
|
|
35
|
+
diagnostic: Pick<AssetDiagnostic, 'adapter' | 'status' | 'reason'>
|
|
36
|
+
) => {
|
|
30
37
|
diagnostics.push({
|
|
31
|
-
assetId,
|
|
32
|
-
adapter:
|
|
33
|
-
status:
|
|
34
|
-
reason:
|
|
38
|
+
assetId: asset.id,
|
|
39
|
+
adapter: diagnostic.adapter,
|
|
40
|
+
status: diagnostic.status,
|
|
41
|
+
reason: diagnostic.reason,
|
|
42
|
+
source: resolveWorkspaceAssetSource(asset),
|
|
35
43
|
packageId: asset.packageId,
|
|
36
44
|
scope: asset.scope,
|
|
37
45
|
instancePath: asset.instancePath,
|
|
@@ -41,6 +49,16 @@ export function buildAdapterAssetPlan(params: {
|
|
|
41
49
|
})
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
for (const assetId of params.options.promptAssetIds ?? []) {
|
|
53
|
+
const asset = params.bundle.assets.find(item => item.id === assetId)
|
|
54
|
+
if (asset == null || asset.kind === 'mcpServer') continue
|
|
55
|
+
pushDiagnostic(asset, {
|
|
56
|
+
adapter: params.adapter,
|
|
57
|
+
status: 'prompt',
|
|
58
|
+
reason: 'Mapped into the generated system prompt.'
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
const selectedMcpNames = resolveSelectedMcpNames(params.bundle, params.options.mcpServers)
|
|
45
63
|
const mcpServers = Object.fromEntries(
|
|
46
64
|
selectedMcpNames.map(name => [name, params.bundle.mcpServers[name].payload.config])
|
|
@@ -48,25 +66,17 @@ export function buildAdapterAssetPlan(params: {
|
|
|
48
66
|
|
|
49
67
|
selectedMcpNames.forEach((name) => {
|
|
50
68
|
const asset = params.bundle.mcpServers[name]
|
|
51
|
-
|
|
52
|
-
assetId: asset.id,
|
|
69
|
+
pushDiagnostic(asset, {
|
|
53
70
|
adapter: params.adapter,
|
|
54
71
|
status: params.adapter === 'claude-code' ? 'native' : 'translated',
|
|
55
72
|
reason: params.adapter === 'claude-code'
|
|
56
73
|
? 'Mapped into adapter MCP settings.'
|
|
57
|
-
: 'Translated into adapter-specific MCP configuration.'
|
|
58
|
-
packageId: asset.packageId,
|
|
59
|
-
scope: asset.scope,
|
|
60
|
-
instancePath: asset.instancePath,
|
|
61
|
-
origin: asset.origin,
|
|
62
|
-
resolvedBy: asset.resolvedBy,
|
|
63
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
74
|
+
: 'Translated into adapter-specific MCP configuration.'
|
|
64
75
|
})
|
|
65
76
|
})
|
|
66
77
|
|
|
67
78
|
params.bundle.hookPlugins.forEach((asset) => {
|
|
68
|
-
|
|
69
|
-
assetId: asset.id,
|
|
79
|
+
pushDiagnostic(asset, {
|
|
70
80
|
adapter: params.adapter,
|
|
71
81
|
status: params.adapter === 'copilot' ? 'translated' : 'native',
|
|
72
82
|
reason: params.adapter === 'claude-code'
|
|
@@ -79,80 +89,46 @@ export function buildAdapterAssetPlan(params: {
|
|
|
79
89
|
? 'Handled by the Vibe Forge task hook bridge.'
|
|
80
90
|
: params.adapter === 'kimi'
|
|
81
91
|
? 'Mapped into the Kimi native hooks bridge.'
|
|
82
|
-
: 'Mapped into the OpenCode native hooks bridge.'
|
|
83
|
-
packageId: asset.packageId,
|
|
84
|
-
scope: asset.scope,
|
|
85
|
-
instancePath: asset.instancePath,
|
|
86
|
-
origin: asset.origin,
|
|
87
|
-
resolvedBy: asset.resolvedBy,
|
|
88
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
92
|
+
: 'Mapped into the OpenCode native hooks bridge.'
|
|
89
93
|
})
|
|
90
94
|
})
|
|
91
95
|
|
|
92
|
-
const selectedSkillAssets =
|
|
96
|
+
const selectedSkillAssets = await resolveSelectedSkillAssetsWithDependencies(params.bundle, params.options.skills)
|
|
93
97
|
if (supportsNativeProjectSkills(params.adapter)) {
|
|
94
98
|
selectedSkillAssets.forEach((asset) => {
|
|
95
|
-
|
|
96
|
-
assetId: asset.id,
|
|
99
|
+
pushDiagnostic(asset, {
|
|
97
100
|
adapter: params.adapter,
|
|
98
101
|
status: 'native',
|
|
99
|
-
reason: resolveNativeSkillDiagnosticReason(params.adapter)
|
|
100
|
-
packageId: asset.packageId,
|
|
101
|
-
scope: asset.scope,
|
|
102
|
-
instancePath: asset.instancePath,
|
|
103
|
-
origin: asset.origin,
|
|
104
|
-
resolvedBy: asset.resolvedBy,
|
|
105
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
102
|
+
reason: resolveNativeSkillDiagnosticReason(params.adapter)
|
|
106
103
|
})
|
|
107
104
|
})
|
|
108
105
|
}
|
|
109
106
|
if (params.adapter === 'opencode') {
|
|
110
107
|
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
111
|
-
|
|
112
|
-
assetId: asset.id,
|
|
108
|
+
pushDiagnostic(asset, {
|
|
113
109
|
adapter: params.adapter,
|
|
114
110
|
status: 'native',
|
|
115
|
-
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
|
|
116
|
-
packageId: asset.packageId,
|
|
117
|
-
scope: asset.scope,
|
|
118
|
-
instancePath: asset.instancePath,
|
|
119
|
-
origin: asset.origin,
|
|
120
|
-
resolvedBy: asset.resolvedBy,
|
|
121
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
111
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
|
|
122
112
|
})
|
|
123
113
|
})
|
|
124
114
|
} else if (params.adapter === 'codex' || params.adapter === 'copilot' || params.adapter === 'kimi') {
|
|
125
115
|
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
126
|
-
|
|
127
|
-
assetId: asset.id,
|
|
116
|
+
pushDiagnostic(asset, {
|
|
128
117
|
adapter: params.adapter,
|
|
129
118
|
status: 'skipped',
|
|
130
119
|
reason: params.adapter === 'codex'
|
|
131
120
|
? 'No stable native Codex mapping exists for this asset kind in V1.'
|
|
132
121
|
: params.adapter === 'copilot'
|
|
133
122
|
? 'No stable native Copilot mapping exists for this asset kind in V1.'
|
|
134
|
-
: 'No stable native Kimi mapping exists for this asset kind in V1.'
|
|
135
|
-
packageId: asset.packageId,
|
|
136
|
-
scope: asset.scope,
|
|
137
|
-
instancePath: asset.instancePath,
|
|
138
|
-
origin: asset.origin,
|
|
139
|
-
resolvedBy: asset.resolvedBy,
|
|
140
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
123
|
+
: 'No stable native Kimi mapping exists for this asset kind in V1.'
|
|
141
124
|
})
|
|
142
125
|
})
|
|
143
126
|
} else if (params.adapter === 'gemini') {
|
|
144
127
|
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
145
|
-
|
|
146
|
-
assetId: asset.id,
|
|
128
|
+
pushDiagnostic(asset, {
|
|
147
129
|
adapter: params.adapter,
|
|
148
130
|
status: 'skipped',
|
|
149
|
-
reason: 'No stable native Gemini mapping exists for this asset kind in V1.'
|
|
150
|
-
packageId: asset.packageId,
|
|
151
|
-
scope: asset.scope,
|
|
152
|
-
instancePath: asset.instancePath,
|
|
153
|
-
origin: asset.origin,
|
|
154
|
-
resolvedBy: asset.resolvedBy,
|
|
155
|
-
taskOverlaySource: asset.taskOverlaySource
|
|
131
|
+
reason: 'No stable native Gemini mapping exists for this asset kind in V1.'
|
|
156
132
|
})
|
|
157
133
|
})
|
|
158
134
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DefinitionSource, WorkspaceAsset } from '@vibe-forge/types'
|
|
2
|
+
|
|
3
|
+
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
4
|
+
|
|
5
|
+
export const resolveWorkspaceAssetSource = (
|
|
6
|
+
asset: Pick<WorkspaceAsset, 'origin' | 'resolvedBy'>
|
|
7
|
+
): DefinitionSource => (
|
|
8
|
+
asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY
|
|
9
|
+
? 'home'
|
|
10
|
+
: asset.origin === 'plugin'
|
|
11
|
+
? 'plugin'
|
|
12
|
+
: 'project'
|
|
13
|
+
)
|
package/src/bundle-internal.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { basename, dirname, extname, resolve } from 'node:path'
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, resolve } from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
resolveDefaultVibeForgeMcpServerConfig
|
|
10
10
|
} from '@vibe-forge/config'
|
|
11
11
|
import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isLegacySkillsConfig,
|
|
14
|
+
resolveProjectAiBaseDir,
|
|
15
|
+
resolveProjectAiEntitiesDir,
|
|
16
|
+
resolveRelativePath
|
|
17
|
+
} from '@vibe-forge/utils'
|
|
13
18
|
import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
|
|
14
19
|
import {
|
|
15
20
|
flattenPluginInstances,
|
|
@@ -28,10 +33,14 @@ import {
|
|
|
28
33
|
resolveSkillIdentifier,
|
|
29
34
|
resolveSpecIdentifier
|
|
30
35
|
} from '@vibe-forge/definition-core'
|
|
36
|
+
import { ensureConfiguredProjectSkills } from './configured-skills'
|
|
37
|
+
import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
38
|
+
import { resolveConfiguredWorkspaceAssets } from './workspaces'
|
|
31
39
|
|
|
32
40
|
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
33
41
|
type OpenCodeOverlayKind = Extract<WorkspaceAssetKind, 'agent' | 'command' | 'mode' | 'nativePlugin'>
|
|
34
42
|
type OpenCodeOverlayAsset<TKind extends OpenCodeOverlayKind> = Extract<WorkspaceAsset, { kind: TKind }>
|
|
43
|
+
type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
|
|
35
44
|
|
|
36
45
|
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
37
46
|
payload: {
|
|
@@ -50,10 +59,147 @@ const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
|
50
59
|
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
51
60
|
)
|
|
52
61
|
|
|
62
|
+
const ENTITY_DIRECTORY_ENTRY_FILES = new Set(['readme.md', 'index.json'])
|
|
63
|
+
const DEFAULT_HOME_SKILL_ROOTS = [
|
|
64
|
+
'~/.agents/skills',
|
|
65
|
+
'~/.claude/skills',
|
|
66
|
+
'~/.config/opencode/skills',
|
|
67
|
+
'~/.gemini/skills'
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
const DEFAULT_ENTITY_PROMPT_FILE_SECTIONS = [
|
|
71
|
+
{
|
|
72
|
+
heading: 'Introduction',
|
|
73
|
+
fileNames: ['INTRODUCTION.md', 'introduction.md', '介绍.md']
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
heading: 'Personality',
|
|
77
|
+
fileNames: ['PERSONALITY.md', 'personality.md', '人格.md']
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
heading: 'Memory',
|
|
81
|
+
fileNames: ['MEMORY.md', 'memory.md', '记忆.md']
|
|
82
|
+
}
|
|
83
|
+
] as const
|
|
84
|
+
|
|
85
|
+
const isMissingFileError = (error: unknown) => (
|
|
86
|
+
error != null &&
|
|
87
|
+
typeof error === 'object' &&
|
|
88
|
+
'code' in error &&
|
|
89
|
+
(error as { code?: unknown }).code === 'ENOENT'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const readOptionalMarkdownBody = async (path: string) => {
|
|
93
|
+
try {
|
|
94
|
+
const content = await readFile(path, 'utf-8')
|
|
95
|
+
return fm<Record<string, never>>(content).body.trim()
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (isMissingFileError(err)) return undefined
|
|
98
|
+
throw err
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const loadDefaultEntityPromptSection = async (
|
|
103
|
+
entityDir: string,
|
|
104
|
+
section: (typeof DEFAULT_ENTITY_PROMPT_FILE_SECTIONS)[number]
|
|
105
|
+
) => {
|
|
106
|
+
for (const fileName of section.fileNames) {
|
|
107
|
+
const body = await readOptionalMarkdownBody(resolve(entityDir, fileName))
|
|
108
|
+
if (body == null || body === '') continue
|
|
109
|
+
|
|
110
|
+
return `## ${section.heading}\n\n${body}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const appendDefaultEntityPromptFiles = async (path: string, body: string) => {
|
|
117
|
+
if (!ENTITY_DIRECTORY_ENTRY_FILES.has(basename(path).toLowerCase())) return body
|
|
118
|
+
|
|
119
|
+
const sections = await Promise.all(
|
|
120
|
+
DEFAULT_ENTITY_PROMPT_FILE_SECTIONS.map(section => loadDefaultEntityPromptSection(dirname(path), section))
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
body.trim(),
|
|
125
|
+
...sections
|
|
126
|
+
]
|
|
127
|
+
.filter((section): section is string => section != null && section !== '')
|
|
128
|
+
.join('\n\n')
|
|
129
|
+
}
|
|
130
|
+
|
|
53
131
|
const resolveDisplayName = (name: string, scope?: string) => (
|
|
54
132
|
scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
|
|
55
133
|
)
|
|
56
134
|
|
|
135
|
+
const toStringList = (value: string | string[] | undefined) => {
|
|
136
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
137
|
+
return [value.trim()]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!Array.isArray(value)) return [] as string[]
|
|
141
|
+
|
|
142
|
+
return value
|
|
143
|
+
.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
|
|
144
|
+
.map(item => item.trim())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const resolveRealHomeDir = (env: NodeJS.ProcessEnv) => {
|
|
148
|
+
const value = env.__VF_PROJECT_REAL_HOME__?.trim() || env.HOME?.trim()
|
|
149
|
+
if (value == null || value === '') return undefined
|
|
150
|
+
return resolve(value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const warnInvalidHomeSkillRoot = (root: string) => {
|
|
154
|
+
console.warn(
|
|
155
|
+
`[vibe-forge] Ignoring invalid skills.homeBridge root "${root}". ` +
|
|
156
|
+
'Use an absolute path or a path starting with "~".'
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
|
|
161
|
+
const [config, userConfig] = configs
|
|
162
|
+
const projectHomeBridge = isLegacySkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
|
|
163
|
+
const userHomeBridge = isLegacySkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
|
|
167
|
+
roots: toStringList(userHomeBridge?.roots ?? projectHomeBridge?.roots)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const resolveHomeSkillRoots = (configs: [Config?, Config?], env: NodeJS.ProcessEnv = process.env) => {
|
|
172
|
+
const homeBridge = resolveHomeBridgeConfig(configs)
|
|
173
|
+
if (homeBridge.enabled === false) return [] as string[]
|
|
174
|
+
|
|
175
|
+
const realHome = resolveRealHomeDir(env)
|
|
176
|
+
if (realHome == null) return [] as string[]
|
|
177
|
+
|
|
178
|
+
const rawRoots = homeBridge.roots.length > 0 ? homeBridge.roots : Array.from(DEFAULT_HOME_SKILL_ROOTS)
|
|
179
|
+
const roots: string[] = []
|
|
180
|
+
const seen = new Set<string>()
|
|
181
|
+
|
|
182
|
+
for (const rawRoot of rawRoots) {
|
|
183
|
+
let resolvedRoot: string | undefined
|
|
184
|
+
|
|
185
|
+
if (rawRoot === '~') {
|
|
186
|
+
resolvedRoot = realHome
|
|
187
|
+
} else if (rawRoot.startsWith('~/')) {
|
|
188
|
+
resolvedRoot = resolve(realHome, rawRoot.slice(2))
|
|
189
|
+
} else if (isAbsolute(rawRoot)) {
|
|
190
|
+
resolvedRoot = resolve(rawRoot)
|
|
191
|
+
} else if (homeBridge.roots.length > 0) {
|
|
192
|
+
warnInvalidHomeSkillRoot(rawRoot)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (resolvedRoot == null || seen.has(resolvedRoot)) continue
|
|
196
|
+
seen.add(resolvedRoot)
|
|
197
|
+
roots.push(resolvedRoot)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return roots
|
|
201
|
+
}
|
|
202
|
+
|
|
57
203
|
const loadWorkspaceConfig = async (cwd: string) => (
|
|
58
204
|
loadConfig({
|
|
59
205
|
cwd,
|
|
@@ -73,6 +219,15 @@ const parseFrontmatterDocument = async <TDefinition extends object>(
|
|
|
73
219
|
}
|
|
74
220
|
}
|
|
75
221
|
|
|
222
|
+
const parseEntityMarkdownDocument = async (path: string): Promise<Definition<Entity>> => {
|
|
223
|
+
const definition = await parseFrontmatterDocument<Entity>(path)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...definition,
|
|
227
|
+
body: await appendDefaultEntityPromptFiles(path, definition.body)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
76
231
|
const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> => {
|
|
77
232
|
const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>
|
|
78
233
|
const promptPath = typeof raw.promptPath === 'string'
|
|
@@ -86,7 +241,7 @@ const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> =
|
|
|
86
241
|
|
|
87
242
|
return {
|
|
88
243
|
path,
|
|
89
|
-
body: prompt,
|
|
244
|
+
body: await appendDefaultEntityPromptFiles(path, prompt),
|
|
90
245
|
attributes: raw as Entity
|
|
91
246
|
}
|
|
92
247
|
}
|
|
@@ -109,6 +264,7 @@ const createDocumentAsset = <
|
|
|
109
264
|
origin: 'workspace' | 'plugin'
|
|
110
265
|
scope?: string
|
|
111
266
|
instance?: ResolvedPluginInstance
|
|
267
|
+
resolvedBy?: string
|
|
112
268
|
}) => {
|
|
113
269
|
const name = ({
|
|
114
270
|
rule: resolveDocumentName,
|
|
@@ -130,7 +286,7 @@ const createDocumentAsset = <
|
|
|
130
286
|
sourcePath: params.definition.path,
|
|
131
287
|
instancePath: params.instance?.instancePath,
|
|
132
288
|
packageId: params.instance?.packageId,
|
|
133
|
-
resolvedBy: params.instance?.resolvedBy,
|
|
289
|
+
resolvedBy: params.resolvedBy ?? params.instance?.resolvedBy,
|
|
134
290
|
taskOverlaySource: params.instance?.overlaySource,
|
|
135
291
|
payload: {
|
|
136
292
|
definition: params.definition
|
|
@@ -238,6 +394,19 @@ const scanWorkspaceDocuments = async (cwd: string) => {
|
|
|
238
394
|
}
|
|
239
395
|
}
|
|
240
396
|
|
|
397
|
+
const scanHomeSkillDocuments = async (configs: [Config?, Config?]) => {
|
|
398
|
+
const roots = resolveHomeSkillRoots(configs)
|
|
399
|
+
if (roots.length === 0) return [] as string[]
|
|
400
|
+
|
|
401
|
+
const scans = await Promise.all(
|
|
402
|
+
roots.map(async root => (
|
|
403
|
+
await glob(['*/SKILL.md'], { cwd: root, absolute: true }).catch(() => [] as string[])
|
|
404
|
+
))
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return scans.flatMap(paths => [...paths].sort((left, right) => left.localeCompare(right)))
|
|
408
|
+
}
|
|
409
|
+
|
|
241
410
|
const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
|
|
242
411
|
const assets = instance.manifest?.assets
|
|
243
412
|
const resolveAssetRoot = (dir: string | undefined, fallback: string) => resolve(instance.rootDir, dir ?? fallback)
|
|
@@ -326,6 +495,24 @@ const assertNoDocumentConflicts = (
|
|
|
326
495
|
}
|
|
327
496
|
}
|
|
328
497
|
|
|
498
|
+
const mergeSkillAssets = (assets: SkillAsset[]) => {
|
|
499
|
+
const directAssets = assets.filter(asset => asset.resolvedBy !== HOME_BRIDGE_RESOLVED_BY)
|
|
500
|
+
const bridgedAssets = assets.filter(asset => asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY)
|
|
501
|
+
|
|
502
|
+
assertNoDocumentConflicts(directAssets)
|
|
503
|
+
|
|
504
|
+
const seen = new Set(directAssets.map(asset => asset.displayName))
|
|
505
|
+
const merged = [...directAssets]
|
|
506
|
+
|
|
507
|
+
for (const asset of bridgedAssets) {
|
|
508
|
+
if (seen.has(asset.displayName)) continue
|
|
509
|
+
seen.add(asset.displayName)
|
|
510
|
+
merged.push(asset)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return merged
|
|
514
|
+
}
|
|
515
|
+
|
|
329
516
|
const assertNoMcpConflicts = (
|
|
330
517
|
assets: Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
|
|
331
518
|
) => {
|
|
@@ -345,9 +532,12 @@ export async function collectWorkspaceAssets(params: {
|
|
|
345
532
|
plugins?: PluginConfig
|
|
346
533
|
overlaySource?: string
|
|
347
534
|
includeManagedPlugins?: boolean
|
|
535
|
+
syncConfiguredSkills?: boolean
|
|
536
|
+
updateConfiguredSkills?: boolean
|
|
348
537
|
useDefaultVibeForgeMcpServer?: boolean
|
|
349
538
|
}): Promise<{
|
|
350
539
|
assets: WorkspaceAsset[]
|
|
540
|
+
configs: [Config?, Config?]
|
|
351
541
|
defaultExcludeMcpServers: string[]
|
|
352
542
|
defaultIncludeMcpServers: string[]
|
|
353
543
|
entities: Array<Extract<WorkspaceAsset, { kind: 'entity' }>>
|
|
@@ -359,8 +549,16 @@ export async function collectWorkspaceAssets(params: {
|
|
|
359
549
|
rules: Array<Extract<WorkspaceAsset, { kind: 'rule' }>>
|
|
360
550
|
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
|
|
361
551
|
specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
|
|
552
|
+
workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
|
|
362
553
|
}> {
|
|
363
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
|
+
}
|
|
364
562
|
const managedPluginConfigs = params.includeManagedPlugins === false
|
|
365
563
|
? undefined
|
|
366
564
|
: toManagedPluginConfig(await listManagedPluginInstalls(params.cwd))
|
|
@@ -374,7 +572,10 @@ export async function collectWorkspaceAssets(params: {
|
|
|
374
572
|
overlaySource: params.overlaySource
|
|
375
573
|
})
|
|
376
574
|
|
|
377
|
-
const localScan = await
|
|
575
|
+
const [localScan, homeSkillPaths] = await Promise.all([
|
|
576
|
+
scanWorkspaceDocuments(params.cwd),
|
|
577
|
+
scanHomeSkillDocuments([config, userConfig])
|
|
578
|
+
])
|
|
378
579
|
const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
|
|
379
580
|
const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
|
|
380
581
|
const pluginOverlayScans = await Promise.all(
|
|
@@ -382,35 +583,43 @@ export async function collectWorkspaceAssets(params: {
|
|
|
382
583
|
)
|
|
383
584
|
|
|
384
585
|
const assets: WorkspaceAsset[] = []
|
|
586
|
+
const skillAssets: SkillAsset[] = []
|
|
385
587
|
|
|
386
588
|
const pushDocumentAssets = async <TKind extends DocumentAssetKind>(
|
|
387
589
|
kind: TKind,
|
|
388
590
|
paths: string[],
|
|
389
591
|
origin: 'workspace' | 'plugin',
|
|
390
592
|
instance?: ResolvedPluginInstance,
|
|
391
|
-
parser?: (path: string) => Promise<any
|
|
593
|
+
parser?: (path: string) => Promise<any>,
|
|
594
|
+
resolvedBy?: string
|
|
392
595
|
) => {
|
|
393
596
|
const definitions = await Promise.all(paths.map(path => (
|
|
394
597
|
parser != null ? parser(path) : parseFrontmatterDocument(path)
|
|
395
598
|
)))
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
)
|
|
599
|
+
const createdAssets = definitions.map(definition =>
|
|
600
|
+
createDocumentAsset({
|
|
601
|
+
cwd: params.cwd,
|
|
602
|
+
kind,
|
|
603
|
+
definition,
|
|
604
|
+
origin,
|
|
605
|
+
scope: instance?.scope,
|
|
606
|
+
instance,
|
|
607
|
+
resolvedBy
|
|
608
|
+
})
|
|
407
609
|
)
|
|
610
|
+
|
|
611
|
+
if (kind === 'skill') {
|
|
612
|
+
skillAssets.push(...createdAssets as SkillAsset[])
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
assets.push(...createdAssets)
|
|
408
617
|
}
|
|
409
618
|
|
|
410
619
|
await pushDocumentAssets('rule', localScan.rulePaths, 'workspace')
|
|
411
620
|
await pushDocumentAssets('skill', localScan.skillPaths, 'workspace')
|
|
412
621
|
await pushDocumentAssets('spec', localScan.specPaths, 'workspace')
|
|
413
|
-
await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace')
|
|
622
|
+
await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace', undefined, parseEntityMarkdownDocument)
|
|
414
623
|
await pushDocumentAssets('entity', localScan.entityJsonPaths, 'workspace', undefined, parseEntityIndexJson)
|
|
415
624
|
|
|
416
625
|
for (let index = 0; index < flattenedPluginInstances.length; index++) {
|
|
@@ -419,9 +628,13 @@ export async function collectWorkspaceAssets(params: {
|
|
|
419
628
|
await pushDocumentAssets('rule', scan.rulePaths, 'plugin', instance)
|
|
420
629
|
await pushDocumentAssets('skill', scan.skillPaths, 'plugin', instance)
|
|
421
630
|
await pushDocumentAssets('spec', scan.specPaths, 'plugin', instance)
|
|
422
|
-
await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance)
|
|
631
|
+
await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
|
|
423
632
|
await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
|
|
424
633
|
}
|
|
634
|
+
await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
|
|
635
|
+
|
|
636
|
+
const skills = mergeSkillAssets(skillAssets)
|
|
637
|
+
assets.push(...skills)
|
|
425
638
|
|
|
426
639
|
const mcpAssets = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
|
|
427
640
|
const addMcpAsset = (
|
|
@@ -507,6 +720,12 @@ export async function collectWorkspaceAssets(params: {
|
|
|
507
720
|
.map(instance => createHookPluginAsset(instance))
|
|
508
721
|
assets.push(...hookPlugins)
|
|
509
722
|
|
|
723
|
+
const workspaces = await resolveConfiguredWorkspaceAssets({
|
|
724
|
+
cwd: params.cwd,
|
|
725
|
+
configs: [config, userConfig]
|
|
726
|
+
})
|
|
727
|
+
assets.push(...workspaces)
|
|
728
|
+
|
|
510
729
|
const opencodeOverlayAssets = flattenedPluginInstances.flatMap((instance, index) => (
|
|
511
730
|
pluginOverlayScans[index].map((entry) =>
|
|
512
731
|
createOpenCodeOverlayAsset({
|
|
@@ -528,13 +747,13 @@ export async function collectWorkspaceAssets(params: {
|
|
|
528
747
|
const entities = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'entity' }> =>
|
|
529
748
|
asset.kind === 'entity'
|
|
530
749
|
)
|
|
531
|
-
const skills = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'skill' }> => asset.kind === 'skill')
|
|
532
750
|
|
|
533
|
-
assertNoDocumentConflicts([...rules, ...specs, ...entities
|
|
751
|
+
assertNoDocumentConflicts([...rules, ...specs, ...entities])
|
|
534
752
|
assertNoMcpConflicts(Array.from(mcpAssets.values()))
|
|
535
753
|
|
|
536
754
|
return {
|
|
537
755
|
assets,
|
|
756
|
+
configs: [config, userConfig],
|
|
538
757
|
defaultExcludeMcpServers: [
|
|
539
758
|
...(config?.defaultExcludeMcpServers ?? []),
|
|
540
759
|
...(userConfig?.defaultExcludeMcpServers ?? [])
|
|
@@ -551,6 +770,7 @@ export async function collectWorkspaceAssets(params: {
|
|
|
551
770
|
pluginInstances,
|
|
552
771
|
rules,
|
|
553
772
|
skills,
|
|
554
|
-
specs
|
|
773
|
+
specs,
|
|
774
|
+
workspaces
|
|
555
775
|
}
|
|
556
776
|
}
|
package/src/bundle.ts
CHANGED
|
@@ -8,12 +8,15 @@ 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)
|
|
14
16
|
|
|
15
17
|
return {
|
|
16
18
|
cwd: params.cwd,
|
|
19
|
+
configs: collected.configs,
|
|
17
20
|
pluginConfigs: collected.pluginConfigs,
|
|
18
21
|
pluginInstances: collected.pluginInstances,
|
|
19
22
|
assets: collected.assets,
|
|
@@ -21,6 +24,7 @@ export async function resolveWorkspaceAssetBundle(params: {
|
|
|
21
24
|
specs: collected.specs,
|
|
22
25
|
entities: collected.entities,
|
|
23
26
|
skills: collected.skills,
|
|
27
|
+
workspaces: collected.workspaces,
|
|
24
28
|
mcpServers: collected.mcpServers,
|
|
25
29
|
hookPlugins: collected.hookPlugins,
|
|
26
30
|
opencodeOverlayAssets: collected.opencodeOverlayAssets,
|