@vibe-forge/workspace-assets 2.0.0 → 2.0.2
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 +56 -1
- package/__tests__/adapter-asset-plan.spec.ts +218 -6
- package/__tests__/bundle.spec.ts +602 -2
- package/__tests__/prompt-builders.spec.ts +39 -0
- package/__tests__/prompt-selection.spec.ts +307 -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 +4 -4
- package/src/adapter-asset-plan.ts +42 -66
- package/src/asset-source.ts +13 -0
- package/src/bundle-internal.ts +226 -21
- package/src/bundle.ts +2 -0
- package/src/home-bridge.ts +1 -0
- package/src/index.ts +3 -0
- package/src/prompt-builders.ts +4 -0
- package/src/prompt-selection.ts +44 -19
- package/src/selection-internal.ts +335 -1
- package/src/skill-dependencies.ts +361 -0
- package/src/skill-registry.ts +329 -0
- package/src/task-tool-guidance.ts +15 -0
- package/src/workspace-config.ts +132 -0
- package/src/workspace-prompt.ts +33 -0
- package/src/workspaces.ts +188 -0
- package/vibe-forge-workspace-assets-2.0.2.tgz +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import type { Config, WorkspaceConfigEntry } from '@vibe-forge/types'
|
|
4
|
+
import { glob } from 'fast-glob'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_WORKSPACE_IGNORES = [
|
|
7
|
+
'**/.git/**',
|
|
8
|
+
'**/.ai/**',
|
|
9
|
+
'**/node_modules/**'
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export interface NormalizedWorkspaceEntry {
|
|
13
|
+
enabled?: boolean
|
|
14
|
+
name?: string
|
|
15
|
+
description?: string
|
|
16
|
+
path?: string
|
|
17
|
+
include?: string[]
|
|
18
|
+
exclude?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
22
|
+
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const toStringList = (value: unknown): string[] => {
|
|
26
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
27
|
+
return [value.trim()]
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(value)) return []
|
|
30
|
+
|
|
31
|
+
return value
|
|
32
|
+
.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
|
|
33
|
+
.map(item => item.trim())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizeWorkspaceEntry = (
|
|
37
|
+
id: string,
|
|
38
|
+
value: string | WorkspaceConfigEntry
|
|
39
|
+
): NormalizedWorkspaceEntry | undefined => {
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
return { path: value }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isRecord(value)) return undefined
|
|
45
|
+
const enabled = typeof value.enabled === 'boolean' ? value.enabled : undefined
|
|
46
|
+
if (enabled === false) return { enabled: false }
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
enabled,
|
|
50
|
+
name: typeof value.name === 'string' && value.name.trim() !== '' ? value.name.trim() : id,
|
|
51
|
+
description: typeof value.description === 'string' && value.description.trim() !== ''
|
|
52
|
+
? value.description.trim()
|
|
53
|
+
: undefined,
|
|
54
|
+
path: typeof value.path === 'string' && value.path.trim() !== '' ? value.path.trim() : undefined,
|
|
55
|
+
include: [
|
|
56
|
+
...toStringList(value.include),
|
|
57
|
+
...toStringList(value.glob),
|
|
58
|
+
...toStringList(value.globs)
|
|
59
|
+
],
|
|
60
|
+
exclude: toStringList(value.exclude)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const normalizeWorkspaceConfig = (config: Config['workspaces'] | undefined) => {
|
|
65
|
+
if (config == null) {
|
|
66
|
+
return {
|
|
67
|
+
include: [] as string[],
|
|
68
|
+
exclude: [] as string[],
|
|
69
|
+
entries: {} as Record<string, NormalizedWorkspaceEntry>
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof config === 'string' || Array.isArray(config)) {
|
|
74
|
+
return {
|
|
75
|
+
include: toStringList(config),
|
|
76
|
+
exclude: [] as string[],
|
|
77
|
+
entries: {} as Record<string, NormalizedWorkspaceEntry>
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isRecord(config)) {
|
|
82
|
+
return {
|
|
83
|
+
include: [] as string[],
|
|
84
|
+
exclude: [] as string[],
|
|
85
|
+
entries: {} as Record<string, NormalizedWorkspaceEntry>
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entries = Object.fromEntries(
|
|
90
|
+
Object.entries(config.entries ?? {})
|
|
91
|
+
.map(([id, value]) => [id, normalizeWorkspaceEntry(id, value)])
|
|
92
|
+
.filter((entry): entry is [string, NormalizedWorkspaceEntry] => entry[1] != null)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
include: [
|
|
97
|
+
...toStringList(config.include),
|
|
98
|
+
...toStringList(config.glob),
|
|
99
|
+
...toStringList(config.globs)
|
|
100
|
+
],
|
|
101
|
+
exclude: toStringList(config.exclude),
|
|
102
|
+
entries
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const isDirectory = async (path: string) => {
|
|
107
|
+
try {
|
|
108
|
+
return (await stat(path)).isDirectory()
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const scanWorkspacePatterns = async (
|
|
115
|
+
cwd: string,
|
|
116
|
+
patterns: string[],
|
|
117
|
+
exclude: string[]
|
|
118
|
+
) => {
|
|
119
|
+
if (patterns.length === 0) return []
|
|
120
|
+
|
|
121
|
+
return await glob(patterns, {
|
|
122
|
+
cwd,
|
|
123
|
+
absolute: true,
|
|
124
|
+
onlyDirectories: true,
|
|
125
|
+
unique: true,
|
|
126
|
+
followSymbolicLinks: true,
|
|
127
|
+
ignore: [
|
|
128
|
+
...DEFAULT_WORKSPACE_IGNORES,
|
|
129
|
+
...exclude
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WorkspaceDefinitionPayload } from '@vibe-forge/types'
|
|
2
|
+
import { CANONICAL_VIBE_FORGE_MCP_SERVER_NAME, resolvePromptPath } from '@vibe-forge/utils'
|
|
3
|
+
|
|
4
|
+
import { buildManagedTaskToolGuidance } from './task-tool-guidance'
|
|
5
|
+
|
|
6
|
+
export const generateWorkspaceRoutePrompt = (
|
|
7
|
+
cwd: string,
|
|
8
|
+
workspaces: WorkspaceDefinitionPayload[]
|
|
9
|
+
) => {
|
|
10
|
+
if (workspaces.length === 0) return ''
|
|
11
|
+
const taskToolGuidance = buildManagedTaskToolGuidance(CANONICAL_VIBE_FORGE_MCP_SERVER_NAME)
|
|
12
|
+
|
|
13
|
+
const workspaceList = workspaces
|
|
14
|
+
.map((workspace) => {
|
|
15
|
+
const description = workspace.description?.trim() || workspace.name?.trim() || workspace.path
|
|
16
|
+
return (
|
|
17
|
+
` - Identifier: ${workspace.id}\n` +
|
|
18
|
+
` - Path: ${resolvePromptPath(cwd, workspace.cwd)}\n` +
|
|
19
|
+
` - Description: ${description}\n`
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
.join('')
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
'<system-prompt>\n' +
|
|
26
|
+
'The project includes the following registered workspaces:\n' +
|
|
27
|
+
`${workspaceList}\n` +
|
|
28
|
+
`When a user request targets one of these workspaces, start a child task with \`${CANONICAL_VIBE_FORGE_MCP_SERVER_NAME}.StartTasks\` using \`type: "workspace"\` and \`name\` set to the workspace identifier. ` +
|
|
29
|
+
'Do not directly edit files inside a registered workspace from the current session unless the user explicitly asks this session to work in that directory.\n' +
|
|
30
|
+
`${taskToolGuidance}\n` +
|
|
31
|
+
'</system-prompt>\n'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { mergeConfigs } from '@vibe-forge/config'
|
|
4
|
+
import type { Config, WorkspaceAsset } from '@vibe-forge/types'
|
|
5
|
+
import { normalizePath, resolveRelativePath } from '@vibe-forge/utils'
|
|
6
|
+
|
|
7
|
+
import { isDirectory, normalizeWorkspaceConfig, scanWorkspacePatterns } from './workspace-config'
|
|
8
|
+
|
|
9
|
+
interface WorkspaceCandidate {
|
|
10
|
+
preferredId?: string
|
|
11
|
+
name?: string
|
|
12
|
+
description?: string
|
|
13
|
+
cwd: string
|
|
14
|
+
path: string
|
|
15
|
+
pattern?: string
|
|
16
|
+
sourcePath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const createWorkspaceAsset = (params: {
|
|
20
|
+
id: string
|
|
21
|
+
name?: string
|
|
22
|
+
description?: string
|
|
23
|
+
cwd: string
|
|
24
|
+
path: string
|
|
25
|
+
pattern?: string
|
|
26
|
+
sourcePath: string
|
|
27
|
+
}) => ({
|
|
28
|
+
id: `workspace:workspace:workspace:${params.id}:${params.path}`,
|
|
29
|
+
kind: 'workspace',
|
|
30
|
+
name: params.id,
|
|
31
|
+
displayName: params.id,
|
|
32
|
+
origin: 'workspace',
|
|
33
|
+
sourcePath: params.sourcePath,
|
|
34
|
+
payload: {
|
|
35
|
+
id: params.id,
|
|
36
|
+
name: params.name,
|
|
37
|
+
description: params.description,
|
|
38
|
+
path: params.path,
|
|
39
|
+
cwd: params.cwd,
|
|
40
|
+
pattern: params.pattern
|
|
41
|
+
}
|
|
42
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'workspace' }>)
|
|
43
|
+
|
|
44
|
+
const addCandidate = (
|
|
45
|
+
candidates: Map<string, WorkspaceCandidate>,
|
|
46
|
+
candidate: WorkspaceCandidate
|
|
47
|
+
) => {
|
|
48
|
+
const key = normalizePath(candidate.cwd)
|
|
49
|
+
const existing = candidates.get(key)
|
|
50
|
+
if (existing == null) {
|
|
51
|
+
candidates.set(key, candidate)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
candidates.set(key, {
|
|
56
|
+
...existing,
|
|
57
|
+
...candidate,
|
|
58
|
+
preferredId: candidate.preferredId ?? existing.preferredId,
|
|
59
|
+
name: candidate.name ?? existing.name,
|
|
60
|
+
description: candidate.description ?? existing.description
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const assignWorkspaceIds = (candidates: WorkspaceCandidate[]) => {
|
|
65
|
+
const preferredIds = new Set(
|
|
66
|
+
candidates
|
|
67
|
+
.map(candidate => candidate.preferredId)
|
|
68
|
+
.filter((value): value is string => value != null && value.trim() !== '')
|
|
69
|
+
)
|
|
70
|
+
const basenameCounts = candidates.reduce<Map<string, number>>((counts, candidate) => {
|
|
71
|
+
if (candidate.preferredId != null) return counts
|
|
72
|
+
const id = basename(candidate.path)
|
|
73
|
+
counts.set(id, (counts.get(id) ?? 0) + 1)
|
|
74
|
+
return counts
|
|
75
|
+
}, new Map())
|
|
76
|
+
const usedIds = new Set<string>()
|
|
77
|
+
|
|
78
|
+
return candidates.map((candidate) => {
|
|
79
|
+
const preferredId = candidate.preferredId?.trim()
|
|
80
|
+
if (preferredId != null && preferredId !== '' && !usedIds.has(preferredId)) {
|
|
81
|
+
usedIds.add(preferredId)
|
|
82
|
+
return [preferredId, candidate] as const
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const basenameId = basename(candidate.path)
|
|
86
|
+
const shouldUsePathId = preferredIds.has(basenameId) || (basenameCounts.get(basenameId) ?? 0) > 1
|
|
87
|
+
const id = shouldUsePathId ? candidate.path : basenameId
|
|
88
|
+
const uniqueId = usedIds.has(id) ? candidate.path : id
|
|
89
|
+
usedIds.add(uniqueId)
|
|
90
|
+
return [uniqueId, candidate] as const
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const resolveConfiguredWorkspaceAssets = async (params: {
|
|
95
|
+
cwd: string
|
|
96
|
+
configs?: [Config?, Config?]
|
|
97
|
+
}): Promise<Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>> => {
|
|
98
|
+
const [config, userConfig] = params.configs ?? [undefined, undefined]
|
|
99
|
+
const mergedConfig = mergeConfigs(config, userConfig)
|
|
100
|
+
const workspaceConfig = normalizeWorkspaceConfig(mergedConfig?.workspaces)
|
|
101
|
+
const sourcePath = resolve(params.cwd, '.ai.config.json')
|
|
102
|
+
const candidates = new Map<string, WorkspaceCandidate>()
|
|
103
|
+
|
|
104
|
+
for (const path of await scanWorkspacePatterns(params.cwd, workspaceConfig.include, workspaceConfig.exclude)) {
|
|
105
|
+
const relativePath = resolveRelativePath(params.cwd, path)
|
|
106
|
+
if (relativePath === '') continue
|
|
107
|
+
addCandidate(candidates, {
|
|
108
|
+
cwd: path,
|
|
109
|
+
path: relativePath,
|
|
110
|
+
sourcePath
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [id, entry] of Object.entries(workspaceConfig.entries)) {
|
|
115
|
+
if (entry.enabled === false) continue
|
|
116
|
+
|
|
117
|
+
if (entry.path != null) {
|
|
118
|
+
const workspaceCwd = resolve(params.cwd, entry.path)
|
|
119
|
+
if (await isDirectory(workspaceCwd)) {
|
|
120
|
+
addCandidate(candidates, {
|
|
121
|
+
preferredId: id,
|
|
122
|
+
name: entry.name,
|
|
123
|
+
description: entry.description,
|
|
124
|
+
cwd: workspaceCwd,
|
|
125
|
+
path: resolveRelativePath(params.cwd, workspaceCwd),
|
|
126
|
+
sourcePath
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (
|
|
132
|
+
const path of await scanWorkspacePatterns(
|
|
133
|
+
params.cwd,
|
|
134
|
+
entry.include ?? [],
|
|
135
|
+
[
|
|
136
|
+
...workspaceConfig.exclude,
|
|
137
|
+
...(entry.exclude ?? [])
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
) {
|
|
141
|
+
const relativePath = resolveRelativePath(params.cwd, path)
|
|
142
|
+
if (relativePath === '') continue
|
|
143
|
+
addCandidate(candidates, {
|
|
144
|
+
name: entry.name,
|
|
145
|
+
description: entry.description,
|
|
146
|
+
cwd: path,
|
|
147
|
+
path: relativePath,
|
|
148
|
+
pattern: entry.include?.join(', '),
|
|
149
|
+
sourcePath
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return assignWorkspaceIds(Array.from(candidates.values()))
|
|
155
|
+
.map(([id, candidate]) =>
|
|
156
|
+
createWorkspaceAsset({
|
|
157
|
+
id,
|
|
158
|
+
name: candidate.name,
|
|
159
|
+
description: candidate.description,
|
|
160
|
+
cwd: candidate.cwd,
|
|
161
|
+
path: candidate.path,
|
|
162
|
+
pattern: candidate.pattern,
|
|
163
|
+
sourcePath: candidate.sourcePath
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const findWorkspaceAsset = (
|
|
169
|
+
workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>,
|
|
170
|
+
ref: string
|
|
171
|
+
) => {
|
|
172
|
+
const normalizedRef = normalizePath(ref.trim())
|
|
173
|
+
const matches = workspaces.filter(workspace =>
|
|
174
|
+
workspace.displayName === normalizedRef ||
|
|
175
|
+
workspace.name === normalizedRef ||
|
|
176
|
+
workspace.payload.id === normalizedRef ||
|
|
177
|
+
workspace.payload.path === normalizedRef ||
|
|
178
|
+
workspace.payload.name === normalizedRef
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if (matches.length === 0) return undefined
|
|
182
|
+
if (matches.length > 1) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Ambiguous workspace reference ${ref}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
return matches[0]
|
|
188
|
+
}
|
|
Binary file
|