@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.
@@ -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
- )
@@ -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
- }