@vibe-forge/workspace-assets 3.2.2-alpha.3 → 3.3.0-rc.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.
@@ -22,7 +22,7 @@ vi.mock('@vibe-forge/utils/skills-cli', async () => {
22
22
  }
23
23
  })
24
24
 
25
- import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
25
+ import { resolveWorkspaceAssetBundle } from '#~/index.js'
26
26
 
27
27
  import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
28
28
 
@@ -93,6 +93,47 @@ describe('resolveWorkspaceAssetBundle', () => {
93
93
  ]))
94
94
  })
95
95
 
96
+ it('skips invalid plugin MCP files and keeps resolving other plugin assets', async () => {
97
+ const workspace = await createWorkspace()
98
+ const invalidMcpPath = join(
99
+ workspace,
100
+ 'node_modules/@vibe-forge/plugin-demo/mcp/broken.yaml'
101
+ )
102
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
103
+
104
+ try {
105
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
106
+ 'package.json': JSON.stringify(
107
+ {
108
+ name: '@vibe-forge/plugin-demo',
109
+ version: '1.0.0'
110
+ },
111
+ null,
112
+ 2
113
+ ),
114
+ 'mcp/browser.json': JSON.stringify({ command: 'npx', args: ['browser-server'] }, null, 2),
115
+ 'mcp/broken.yaml': 'command: npx\nargs: [broken\n'
116
+ })
117
+
118
+ const bundle = await resolveWorkspaceAssetBundle({
119
+ cwd: workspace,
120
+ configs: [{
121
+ plugins: [
122
+ { id: 'demo', scope: 'demo' }
123
+ ]
124
+ }, undefined],
125
+ useDefaultVibeForgeMcpServer: false
126
+ })
127
+
128
+ expect(Object.keys(bundle.mcpServers)).toEqual(['demo/browser'])
129
+ expect(warnSpy.mock.calls.map(([message]) => String(message))).toEqual(expect.arrayContaining([
130
+ expect.stringContaining(`Ignoring invalid mcpServer asset "${invalidMcpPath}"`)
131
+ ]))
132
+ } finally {
133
+ warnSpy.mockRestore()
134
+ }
135
+ })
136
+
96
137
  it('loads workspace assets from the env-configured ai base dir', async () => {
97
138
  const workspace = await createWorkspace()
98
139
  const previousBaseDir = process.env.__VF_PROJECT_AI_BASE_DIR__
@@ -187,6 +228,40 @@ describe('resolveWorkspaceAssetBundle', () => {
187
228
  }))
188
229
  })
189
230
 
231
+ it('skips invalid home-bridged skill frontmatter and keeps resolving other skills', async () => {
232
+ const workspace = await createWorkspace()
233
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
234
+ const invalidSkillPath = join(realHome!, '.agents/skills/broken/SKILL.md')
235
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
236
+
237
+ try {
238
+ await writeDocument(
239
+ invalidSkillPath,
240
+ '---\ndescription: Use minimal writing principles: understand the reader\n---\nBroken skill body'
241
+ )
242
+ await writeDocument(
243
+ join(realHome!, '.agents/skills/research/SKILL.md'),
244
+ '---\ndescription: Valid skill\n---\nValid skill body'
245
+ )
246
+
247
+ const bundle = await resolveWorkspaceAssetBundle({
248
+ cwd: workspace,
249
+ configs: [undefined, undefined],
250
+ useDefaultVibeForgeMcpServer: false
251
+ })
252
+
253
+ expect(bundle.skills.map(asset => asset.displayName)).toEqual(['research'])
254
+ expect(warnSpy.mock.calls.map(([message]) => String(message))).toEqual(expect.arrayContaining([
255
+ expect.stringContaining(`Ignoring invalid skill asset "${invalidSkillPath}"`)
256
+ ]))
257
+ expect(warnSpy.mock.calls.map(([message]) => String(message))).toEqual(expect.arrayContaining([
258
+ expect.stringContaining('quote plain strings containing ": "')
259
+ ]))
260
+ } finally {
261
+ warnSpy.mockRestore()
262
+ }
263
+ })
264
+
190
265
  it('can disable the home skill bridge entirely', async () => {
191
266
  const workspace = await createWorkspace()
192
267
  const realHome = process.env.__VF_PROJECT_REAL_HOME__
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "3.2.2-alpha.3",
3
+ "version": "3.3.0-rc.0",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -29,10 +29,10 @@
29
29
  "fast-glob": "^3.3.3",
30
30
  "front-matter": "^4.0.2",
31
31
  "js-yaml": "^4.1.1",
32
- "@vibe-forge/config": "3.2.3-alpha.3",
33
- "@vibe-forge/types": "3.2.3-alpha.1",
34
- "@vibe-forge/definition-core": "3.2.0",
35
- "@vibe-forge/utils": "3.2.3-alpha.3"
32
+ "@vibe-forge/config": "3.3.0-rc.0",
33
+ "@vibe-forge/utils": "3.3.0-rc.0",
34
+ "@vibe-forge/types": "3.3.0-rc.0",
35
+ "@vibe-forge/definition-core": "3.3.0-rc.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -159,6 +159,20 @@ const warnInvalidHomeSkillRoot = (root: string) => {
159
159
  )
160
160
  }
161
161
 
162
+ const formatErrorMessage = (error: unknown) => (
163
+ (error instanceof Error ? error.message : String(error))
164
+ .split(/\r?\n/u)[0]
165
+ ?.trim() ?? 'Unknown error'
166
+ )
167
+
168
+ const warnInvalidWorkspaceAsset = (kind: WorkspaceAssetKind, path: string, error: unknown) => {
169
+ console.warn(
170
+ `[vibe-forge] Ignoring invalid ${kind} asset "${path}". ` +
171
+ `${formatErrorMessage(error)}. ` +
172
+ 'Check the asset frontmatter or structured config syntax; quote plain strings containing ": ".'
173
+ )
174
+ }
175
+
162
176
  const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
163
177
  const [config, userConfig] = configs
164
178
  const projectHomeBridge = resolveSkillsHomeBridge(config)
@@ -221,6 +235,19 @@ const parseFrontmatterDocument = async <TDefinition extends object>(
221
235
  }
222
236
  }
223
237
 
238
+ const parseOptionalDocument = async <TDefinition extends object>(
239
+ kind: DocumentAssetKind,
240
+ path: string,
241
+ parser: (path: string) => Promise<Definition<TDefinition>>
242
+ ) => {
243
+ try {
244
+ return await parser(path)
245
+ } catch (error) {
246
+ warnInvalidWorkspaceAsset(kind, path, error)
247
+ return undefined
248
+ }
249
+ }
250
+
224
251
  const parseEntityMarkdownDocument = async (path: string): Promise<Definition<Entity>> => {
225
252
  const definition = await parseFrontmatterDocument<Entity>(path)
226
253
 
@@ -256,6 +283,15 @@ const parseStructuredMcpFile = async (path: string) => {
256
283
  : JSON.parse(raw)
257
284
  }
258
285
 
286
+ const parseOptionalStructuredMcpFile = async (path: string) => {
287
+ try {
288
+ return await parseStructuredMcpFile(path)
289
+ } catch (error) {
290
+ warnInvalidWorkspaceAsset('mcpServer', path, error)
291
+ return undefined
292
+ }
293
+ }
294
+
259
295
  const createDocumentAsset = <
260
296
  TKind extends DocumentAssetKind,
261
297
  TDefinition extends { path: string; attributes: { name?: string } },
@@ -659,10 +695,12 @@ export async function collectWorkspaceAssets(params: {
659
695
  resolvedBy?: string
660
696
  ) => {
661
697
  const definitions = await Promise.all(paths.map(path => (
662
- parser != null ? parser(path) : parseFrontmatterDocument(path)
698
+ parseOptionalDocument(kind, path, parser != null ? parser : parseFrontmatterDocument)
663
699
  )))
664
- const createdAssets = definitions.map(definition =>
665
- createDocumentAsset({
700
+ const createdAssets = definitions.flatMap((definition) => {
701
+ if (definition == null) return []
702
+
703
+ return [createDocumentAsset({
666
704
  cwd: params.cwd,
667
705
  kind,
668
706
  definition,
@@ -670,8 +708,8 @@ export async function collectWorkspaceAssets(params: {
670
708
  scope: instance?.scope,
671
709
  instance,
672
710
  resolvedBy
673
- })
674
- )
711
+ })]
712
+ })
675
713
 
676
714
  if (kind === 'skill') {
677
715
  skillAssets.push(...createdAssets as SkillAsset[])
@@ -770,7 +808,7 @@ export async function collectWorkspaceAssets(params: {
770
808
  const instance = flattenedPluginInstances[index]
771
809
  const scan = pluginScans[index]
772
810
  for (const path of scan.mcpPaths) {
773
- const parsed = await parseStructuredMcpFile(path)
811
+ const parsed = await parseOptionalStructuredMcpFile(path)
774
812
  if (!isRecord(parsed)) continue
775
813
  const fileName = basename(path, extname(path))
776
814
  const name = typeof parsed.name === 'string' && parsed.name.trim() !== ''
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- configured skill resolution keeps matching, warnings, and CLI sync policy together. */
1
2
  import { access } from 'node:fs/promises'
2
3
  import { isAbsolute, join, resolve as resolvePath } from 'node:path'
3
4
  import process from 'node:process'