@vibe-forge/workspace-assets 0.9.2 → 0.10.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.
@@ -1,10 +1,11 @@
1
1
  import process from 'node:process'
2
+ import { join } from 'node:path'
2
3
 
3
4
  import { describe, expect, it } from 'vitest'
4
5
 
5
6
  import { resolveWorkspaceAssetBundle } from '#~/index.js'
6
7
 
7
- import { createWorkspace, installPluginPackage } from './test-helpers'
8
+ import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
8
9
 
9
10
  describe('resolveWorkspaceAssetBundle', () => {
10
11
  it('loads npm plugin assets via the package-id fallback and exposes OpenCode overlays', async () => {
@@ -67,6 +68,58 @@ describe('resolveWorkspaceAssetBundle', () => {
67
68
  ]))
68
69
  })
69
70
 
71
+ it('auto-loads managed Claude plugins from .ai/plugins as directory plugins', async () => {
72
+ const workspace = await createWorkspace()
73
+
74
+ await writeDocument(
75
+ join(workspace, '.ai/plugins/demo/.vf-plugin.json'),
76
+ JSON.stringify(
77
+ {
78
+ version: 1,
79
+ adapter: 'claude',
80
+ name: 'demo',
81
+ scope: 'demo',
82
+ installedAt: new Date().toISOString(),
83
+ source: {
84
+ type: 'path',
85
+ path: './demo'
86
+ },
87
+ nativePluginPath: 'native',
88
+ vibeForgePluginPath: 'vibe-forge'
89
+ },
90
+ null,
91
+ 2
92
+ )
93
+ )
94
+ await writeDocument(
95
+ join(workspace, '.ai/plugins/demo/vibe-forge/skills/research/SKILL.md'),
96
+ '---\ndescription: 检索资料\n---\n阅读 README.md'
97
+ )
98
+ await writeDocument(
99
+ join(workspace, '.ai/plugins/demo/vibe-forge/mcp/browser.json'),
100
+ JSON.stringify({ command: 'npx', args: ['browser-server'] }, null, 2)
101
+ )
102
+ await writeDocument(
103
+ join(workspace, '.ai/plugins/demo/vibe-forge/hooks.js'),
104
+ 'module.exports = { name: "demo-managed" }\n'
105
+ )
106
+
107
+ const bundle = await resolveWorkspaceAssetBundle({
108
+ cwd: workspace,
109
+ configs: [undefined, undefined],
110
+ useDefaultVibeForgeMcpServer: false
111
+ })
112
+
113
+ expect(bundle.skills.map(asset => asset.displayName)).toContain('demo/research')
114
+ expect(Object.keys(bundle.mcpServers)).toContain('demo/browser')
115
+ expect(bundle.hookPlugins).toEqual(expect.arrayContaining([
116
+ expect.objectContaining({
117
+ scope: 'demo',
118
+ origin: 'plugin'
119
+ })
120
+ ]))
121
+ })
122
+
70
123
  it('adds the built-in Vibe Forge MCP server when enabled and omits it when disabled', async () => {
71
124
  const workspace = await createWorkspace()
72
125
 
@@ -389,4 +389,60 @@ describe('resolvePromptAssetSelection', () => {
389
389
  expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
390
390
  expect(options.systemPrompt).not.toContain('先读 README.md')
391
391
  })
392
+
393
+ it('lets spec plugin overrides exclude managed plugins when mode is override', async () => {
394
+ const workspace = await createWorkspace()
395
+
396
+ await writeDocument(
397
+ join(workspace, '.ai/plugins/demo/.vf-plugin.json'),
398
+ JSON.stringify({
399
+ version: 1,
400
+ adapter: 'claude',
401
+ name: 'demo',
402
+ scope: 'demo',
403
+ installedAt: new Date().toISOString(),
404
+ source: {
405
+ type: 'path',
406
+ path: './demo'
407
+ },
408
+ nativePluginPath: 'native',
409
+ vibeForgePluginPath: 'vibe-forge'
410
+ }, null, 2)
411
+ )
412
+ await writeDocument(
413
+ join(workspace, '.ai/plugins/demo/vibe-forge/skills/research/SKILL.md'),
414
+ [
415
+ '---',
416
+ 'description: 托管插件技能',
417
+ '---',
418
+ '先读插件文档'
419
+ ].join('\n')
420
+ )
421
+ await writeDocument(
422
+ join(workspace, '.ai/specs/isolated/index.md'),
423
+ [
424
+ '---',
425
+ 'description: 隔离 spec',
426
+ 'plugins:',
427
+ ' mode: override',
428
+ ' list: []',
429
+ '---',
430
+ '执行隔离流程'
431
+ ].join('\n')
432
+ )
433
+
434
+ const bundle = await resolveWorkspaceAssetBundle({
435
+ cwd: workspace,
436
+ useDefaultVibeForgeMcpServer: false
437
+ })
438
+ const [, options] = await resolvePromptAssetSelection({
439
+ bundle,
440
+ type: 'spec',
441
+ name: 'isolated'
442
+ })
443
+
444
+ expect(bundle.skills.map(asset => asset.displayName)).toContain('demo/research')
445
+ expect(options.assetBundle?.skills).toEqual([])
446
+ expect(options.systemPrompt).not.toContain('demo/research')
447
+ })
392
448
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "0.9.2",
3
+ "version": "0.10.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": "^0.9.0",
33
- "@vibe-forge/definition-core": "^0.9.0",
34
- "@vibe-forge/utils": "^0.9.0",
35
- "@vibe-forge/types": "^0.9.0"
32
+ "@vibe-forge/definition-core": "^0.10.0",
33
+ "@vibe-forge/config": "^0.10.0",
34
+ "@vibe-forge/types": "^0.10.0",
35
+ "@vibe-forge/utils": "^0.10.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -10,11 +10,12 @@ import {
10
10
  } from '@vibe-forge/config'
11
11
  import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
12
12
  import { resolveRelativePath } from '@vibe-forge/utils'
13
+ import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
13
14
  import {
14
15
  flattenPluginInstances,
15
16
  mergePluginConfigs,
16
17
  resolveConfiguredPluginInstances,
17
- resolvePluginHooksEntryPath
18
+ resolvePluginHooksEntryPathForInstance
18
19
  } from '@vibe-forge/utils/plugin-resolver'
19
20
  import type { ResolvedPluginInstance } from '@vibe-forge/utils/plugin-resolver'
20
21
  import { glob } from 'fast-glob'
@@ -341,6 +342,7 @@ export async function collectWorkspaceAssets(params: {
341
342
  configs?: [Config?, Config?]
342
343
  plugins?: PluginConfig
343
344
  overlaySource?: string
345
+ includeManagedPlugins?: boolean
344
346
  useDefaultVibeForgeMcpServer?: boolean
345
347
  }): Promise<{
346
348
  assets: WorkspaceAsset[]
@@ -357,7 +359,13 @@ export async function collectWorkspaceAssets(params: {
357
359
  specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
358
360
  }> {
359
361
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
360
- const pluginConfigs = params.plugins ?? mergePluginConfigs(config?.plugins, userConfig?.plugins)
362
+ const managedPluginConfigs = params.includeManagedPlugins === false
363
+ ? undefined
364
+ : toManagedPluginConfig(await listManagedPluginInstalls(params.cwd))
365
+ const pluginConfigs = mergePluginConfigs(
366
+ params.plugins ?? mergePluginConfigs(config?.plugins, userConfig?.plugins),
367
+ managedPluginConfigs
368
+ )
361
369
  const pluginInstances = await resolveConfiguredPluginInstances({
362
370
  cwd: params.cwd,
363
371
  plugins: pluginConfigs,
@@ -493,9 +501,7 @@ export async function collectWorkspaceAssets(params: {
493
501
  }
494
502
 
495
503
  const hookPlugins = flattenedPluginInstances
496
- .filter(instance =>
497
- instance.packageId != null && resolvePluginHooksEntryPath(params.cwd, instance.packageId) != null
498
- )
504
+ .filter(instance => resolvePluginHooksEntryPathForInstance(params.cwd, instance) != null)
499
505
  .map(instance => createHookPluginAsset(instance))
500
506
  assets.push(...hookPlugins)
501
507
 
package/src/bundle.ts CHANGED
@@ -7,6 +7,7 @@ export async function resolveWorkspaceAssetBundle(params: {
7
7
  configs?: [Config?, Config?]
8
8
  plugins?: PluginConfig
9
9
  overlaySource?: string
10
+ includeManagedPlugins?: boolean
10
11
  useDefaultVibeForgeMcpServer?: boolean
11
12
  }): Promise<WorkspaceAssetBundle> {
12
13
  const collected = await collectWorkspaceAssets(params)
@@ -69,7 +69,8 @@ export async function resolvePromptAssetSelection(params: {
69
69
  effectiveBundle = await resolveWorkspaceAssetBundle({
70
70
  cwd: params.bundle.cwd,
71
71
  plugins: resolvePluginOverlay(params.bundle.pluginConfigs, pluginOverlay),
72
- overlaySource: `${params.type}:${baseTarget.displayName}`
72
+ overlaySource: `${params.type}:${baseTarget.displayName}`,
73
+ includeManagedPlugins: pluginOverlay.mode !== 'override'
73
74
  })
74
75
  }
75
76
 
@@ -219,11 +219,7 @@ export const resolveSelectedMcpNames = (
219
219
  const includeRefs = selection?.include ??
220
220
  (bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined)
221
221
  const excludeRefs = selection?.exclude ??
222
- (
223
- selection?.include == null && bundle.defaultExcludeMcpServers.length > 0
224
- ? bundle.defaultExcludeMcpServers
225
- : undefined
226
- )
222
+ (bundle.defaultExcludeMcpServers.length > 0 ? bundle.defaultExcludeMcpServers : undefined)
227
223
 
228
224
  const resolveRefs = (refs: string[] | undefined) => {
229
225
  if (refs == null || refs.length === 0) return undefined
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import type { WorkspaceAssetBundle } from '@vibe-forge/types'
4
-
5
- import { resolveSelectedMcpNames } from '#~/selection-internal.js'
6
-
7
- const createBundle = (): WorkspaceAssetBundle => ({
8
- cwd: '/tmp/workspace',
9
- pluginConfigs: [],
10
- pluginInstances: [],
11
- assets: [],
12
- rules: [],
13
- specs: [],
14
- entities: [],
15
- skills: [],
16
- hookPlugins: [],
17
- opencodeOverlayAssets: [],
18
- defaultIncludeMcpServers: [],
19
- defaultExcludeMcpServers: ['TypeScriptLanguageService'],
20
- mcpServers: {
21
- TypeScriptLanguageService: {
22
- kind: 'mcpServer',
23
- id: 'mcp:TypeScriptLanguageService',
24
- scope: 'workspace',
25
- name: 'TypeScriptLanguageService',
26
- displayName: 'TypeScriptLanguageService',
27
- source: '/tmp/workspace/.ai.config.json',
28
- command: 'node',
29
- args: ['tslspmcp']
30
- }
31
- }
32
- })
33
-
34
- describe('resolveSelectedMcpNames', () => {
35
- it('lets an explicit include override default excludes', () => {
36
- const selected = resolveSelectedMcpNames(createBundle(), {
37
- include: ['TypeScriptLanguageService']
38
- })
39
-
40
- expect(selected).toEqual(['TypeScriptLanguageService'])
41
- })
42
- })