@vibe-forge/workspace-assets 0.9.1-alpha.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/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 +198 -0
- package/__tests__/prompt-selection.spec.ts +362 -14
- 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 +271 -0
- package/src/document-assets.ts +0 -191
- package/src/helpers.ts +0 -35
- package/src/plugin-assets.ts +0 -175
package/src/document-assets.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
)
|
package/src/plugin-assets.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { basename, extname } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import type { Config, WorkspaceAsset, WorkspaceAssetAdapter } from '@vibe-forge/types'
|
|
5
|
-
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
6
|
-
import { glob } from 'fast-glob'
|
|
7
|
-
import yaml from 'js-yaml'
|
|
8
|
-
|
|
9
|
-
import { isPluginEnabled, resolvePluginIdFromPath } from './helpers'
|
|
10
|
-
import type { WorkspaceOpenCodeOverlayAsset } from './internal-types'
|
|
11
|
-
|
|
12
|
-
const parseStructuredDocument = async (path: string) => {
|
|
13
|
-
const raw = await readFile(path, 'utf8')
|
|
14
|
-
const extension = extname(path).toLowerCase()
|
|
15
|
-
if (extension === '.yaml' || extension === '.yml') {
|
|
16
|
-
return yaml.load(raw)
|
|
17
|
-
}
|
|
18
|
-
return JSON.parse(raw)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const loadPluginMcpAssets = async (
|
|
22
|
-
cwd: string,
|
|
23
|
-
enabledPlugins: Record<string, boolean>
|
|
24
|
-
): Promise<Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>> => {
|
|
25
|
-
const paths = await glob([
|
|
26
|
-
'.ai/plugins/*/mcp/*.json',
|
|
27
|
-
'.ai/plugins/*/mcp/*.yaml',
|
|
28
|
-
'.ai/plugins/*/mcp/*.yml'
|
|
29
|
-
], {
|
|
30
|
-
cwd,
|
|
31
|
-
absolute: true
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const entries = await Promise.all(paths.map(async (path) => {
|
|
35
|
-
const pluginId = resolvePluginIdFromPath(cwd, path)
|
|
36
|
-
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
37
|
-
|
|
38
|
-
const parsed = await parseStructuredDocument(path)
|
|
39
|
-
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
|
40
|
-
|
|
41
|
-
const record = parsed as Record<string, unknown>
|
|
42
|
-
const name = typeof record.name === 'string' && record.name.trim() !== ''
|
|
43
|
-
? record.name.trim()
|
|
44
|
-
: basename(path, extname(path))
|
|
45
|
-
const { name: _name, ...config } = record
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
id: `mcpServer:${resolveRelativePath(cwd, path)}`,
|
|
49
|
-
kind: 'mcpServer',
|
|
50
|
-
pluginId,
|
|
51
|
-
origin: 'plugin',
|
|
52
|
-
scope: 'workspace',
|
|
53
|
-
enabled: true,
|
|
54
|
-
targets: ['claude-code', 'codex', 'opencode'],
|
|
55
|
-
payload: {
|
|
56
|
-
name,
|
|
57
|
-
config: config as NonNullable<Config['mcpServers']>[string]
|
|
58
|
-
}
|
|
59
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
|
|
60
|
-
}))
|
|
61
|
-
|
|
62
|
-
return entries.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const loadOpenCodeOverlayAssets = async (
|
|
66
|
-
cwd: string,
|
|
67
|
-
enabledPlugins: Record<string, boolean>
|
|
68
|
-
): Promise<WorkspaceOpenCodeOverlayAsset[]> => {
|
|
69
|
-
const paths = await glob([
|
|
70
|
-
'.ai/plugins/*/opencode/plugins/*',
|
|
71
|
-
'.ai/plugins/*/opencode/agents/*',
|
|
72
|
-
'.ai/plugins/*/opencode/commands/*',
|
|
73
|
-
'.ai/plugins/*/opencode/modes/*'
|
|
74
|
-
], {
|
|
75
|
-
cwd,
|
|
76
|
-
absolute: true,
|
|
77
|
-
onlyFiles: false
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
return paths
|
|
81
|
-
.map((path) => {
|
|
82
|
-
const relativePath = resolveRelativePath(cwd, path)
|
|
83
|
-
const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\/opencode\/(plugins|agents|commands|modes)\/([^/]+)$/)
|
|
84
|
-
if (!match) return undefined
|
|
85
|
-
|
|
86
|
-
const [, pluginId, rawFolder, entryName] = match
|
|
87
|
-
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
88
|
-
|
|
89
|
-
const base = {
|
|
90
|
-
pluginId,
|
|
91
|
-
origin: 'plugin' as const,
|
|
92
|
-
scope: 'workspace' as const,
|
|
93
|
-
enabled: true,
|
|
94
|
-
targets: ['opencode'] as WorkspaceAssetAdapter[],
|
|
95
|
-
payload: {
|
|
96
|
-
sourcePath: path,
|
|
97
|
-
entryName,
|
|
98
|
-
targetSubpath: `${rawFolder}/${entryName}`
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (rawFolder === 'plugins') {
|
|
103
|
-
return {
|
|
104
|
-
id: `nativePlugin:${relativePath}`,
|
|
105
|
-
kind: 'nativePlugin',
|
|
106
|
-
...base
|
|
107
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (rawFolder === 'agents') {
|
|
111
|
-
return {
|
|
112
|
-
id: `agent:${relativePath}`,
|
|
113
|
-
kind: 'agent',
|
|
114
|
-
...base
|
|
115
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'agent' }>
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (rawFolder === 'commands') {
|
|
119
|
-
return {
|
|
120
|
-
id: `command:${relativePath}`,
|
|
121
|
-
kind: 'command',
|
|
122
|
-
...base
|
|
123
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'command' }>
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
id: `mode:${relativePath}`,
|
|
128
|
-
kind: 'mode',
|
|
129
|
-
...base
|
|
130
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'mode' }>
|
|
131
|
-
})
|
|
132
|
-
.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const createHookPluginAssets = (
|
|
136
|
-
config: Config['plugins'],
|
|
137
|
-
enabledPlugins: Record<string, boolean>,
|
|
138
|
-
scope: Extract<WorkspaceAsset['scope'], 'project' | 'user'>
|
|
139
|
-
): Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>> => {
|
|
140
|
-
if (config == null || Array.isArray(config)) return [] as Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>>
|
|
141
|
-
|
|
142
|
-
return Object.entries(config)
|
|
143
|
-
.filter((entry) => enabledPlugins[entry[0]] !== false)
|
|
144
|
-
.map(([pluginId, pluginConfig]) => ({
|
|
145
|
-
id: `hookPlugin:${scope}:${pluginId}`,
|
|
146
|
-
kind: 'hookPlugin',
|
|
147
|
-
pluginId,
|
|
148
|
-
origin: 'config',
|
|
149
|
-
scope,
|
|
150
|
-
enabled: true,
|
|
151
|
-
targets: ['claude-code', 'codex', 'opencode'],
|
|
152
|
-
payload: {
|
|
153
|
-
packageName: pluginId,
|
|
154
|
-
config: pluginConfig
|
|
155
|
-
}
|
|
156
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export const createClaudeNativePluginAssets = (
|
|
160
|
-
enabledPlugins: Record<string, boolean>
|
|
161
|
-
): Array<Extract<WorkspaceAsset, { kind: 'nativePlugin' }>> => {
|
|
162
|
-
return Object.entries(enabledPlugins).map(([pluginId, enabled]) => ({
|
|
163
|
-
id: `nativePlugin:claude-code:${pluginId}`,
|
|
164
|
-
kind: 'nativePlugin',
|
|
165
|
-
pluginId,
|
|
166
|
-
origin: 'config',
|
|
167
|
-
scope: 'project',
|
|
168
|
-
enabled,
|
|
169
|
-
targets: ['claude-code'],
|
|
170
|
-
payload: {
|
|
171
|
-
name: pluginId,
|
|
172
|
-
enabled
|
|
173
|
-
}
|
|
174
|
-
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>))
|
|
175
|
-
}
|