@@ -126,7 +126,7 @@ async function emitCursor(
126126 outDir : string ,
127127) : Promise < Artifact > {
128128 const marketplaceDir = targetConfig . marketplaceDir ?? ".cursor-plugin" ;
129- const version = project . config . version ;
129+ const version = targetConfig . version ?? project . config . version ;
130130 const files = new Map < string , string | Buffer > ( ) ;
131131
132132 const plugins = await emitPlugins ( project , target , targetConfig , files , {
@@ -143,7 +143,7 @@ async function emitCursor(
143143 ) =>
144144 cursorPluginManifest (
145145 metadata ,
146- version ,
146+ pluginConfig . version ?? version ,
147147 pluginName ,
148148 pluginConfig ,
149149 componentDirs ,
@@ -153,20 +153,25 @@ async function emitCursor(
153153 mcp : "file" ,
154154 } ) ;
155155
156- const marketplace = {
157- name : project . config . name ,
158- owner : project . config . metadata ?. owner ?? project . config . metadata ?. author ,
159- metadata : {
160- description : project . config . metadata ?. description ,
161- keywords : project . config . metadata ?. keywords ,
162- } ,
163- plugins,
164- version,
165- ...targetConfig . manifest ,
166- } ;
156+ const marketplace = stripUndefined (
157+ deepMerge (
158+ {
159+ name : project . config . name ,
160+ owner :
161+ project . config . metadata ?. owner ?? project . config . metadata ?. author ,
162+ metadata : {
163+ description : project . config . metadata ?. description ,
164+ keywords : project . config . metadata ?. keywords ,
165+ } ,
166+ plugins,
167+ version,
168+ } ,
169+ targetConfig . manifest ?? { } ,
170+ ) ,
171+ ) ;
167172 files . set (
168173 toPosix ( path . join ( marketplaceDir , "marketplace.json" ) ) ,
169- json ( stripUndefined ( marketplace ) ) ,
174+ json ( marketplace ) ,
170175 ) ;
171176
172177 return artifact ( target , outDir , files ) ;
@@ -180,7 +185,7 @@ async function emitClaude(
180185) : Promise < Artifact > {
181186 const marketplaceDir = targetConfig . marketplaceDir ?? ".claude-plugin" ;
182187 const pluginRoot = targetConfig . pluginRoot ?? "plugins" ;
183- const version = project . config . version ;
188+ const version = targetConfig . version ?? project . config . version ;
184189 const files = new Map < string , string | Buffer > ( ) ;
185190
186191 const plugins = await emitPlugins ( project , target , targetConfig , files , {
@@ -189,23 +194,33 @@ async function emitClaude(
189194 pluginManifestPath : ( pluginPath ) =>
190195 path . join ( pluginPath , marketplaceDir , "plugin.json" ) ,
191196 buildManifest : ( metadata , pluginName , pluginConfig ) =>
192- claudePluginManifest ( metadata , version , pluginName , pluginConfig ) ,
197+ claudePluginManifest (
198+ metadata ,
199+ pluginConfig . version ?? version ,
200+ pluginName ,
201+ pluginConfig ,
202+ ) ,
193203 entrySource : ( pluginPath ) => `./${ pluginPath } ` ,
194204 mcp : "file" ,
195205 } ) ;
196206
197- const marketplace = {
198- $schema : "https://anthropic.com/claude-code/marketplace.schema.json" ,
199- name : project . config . name ,
200- version,
201- description : project . config . metadata ?. description ,
202- owner : project . config . metadata ?. owner ?? project . config . metadata ?. author ,
203- plugins,
204- ...targetConfig . manifest ,
205- } ;
207+ const marketplace = stripUndefined (
208+ deepMerge (
209+ {
210+ $schema : "https://anthropic.com/claude-code/marketplace.schema.json" ,
211+ name : project . config . name ,
212+ version,
213+ description : project . config . metadata ?. description ,
214+ owner :
215+ project . config . metadata ?. owner ?? project . config . metadata ?. author ,
216+ plugins,
217+ } ,
218+ targetConfig . manifest ?? { } ,
219+ ) ,
220+ ) ;
206221 files . set (
207222 toPosix ( path . join ( marketplaceDir , "marketplace.json" ) ) ,
208- json ( stripUndefined ( marketplace ) ) ,
223+ json ( marketplace ) ,
209224 ) ;
210225
211226 return artifact ( target , outDir , files ) ;
@@ -217,7 +232,7 @@ async function emitGemini(
217232 targetConfig : TargetConfig ,
218233 outDir : string ,
219234) : Promise < Artifact > {
220- const version = project . config . version ;
235+ const version = targetConfig . version ?? project . config . version ;
221236 const files = new Map < string , string | Buffer > ( ) ;
222237
223238 await emitPlugins ( project , target , targetConfig , files , {
@@ -226,7 +241,13 @@ async function emitGemini(
226241 pluginManifestPath : ( pluginPath ) =>
227242 path . join ( pluginPath , "gemini-extension.json" ) ,
228243 buildManifest : ( metadata , pluginName , pluginConfig , _componentDirs , mcp ) =>
229- geminiExtensionManifest ( metadata , version , pluginName , pluginConfig , mcp ) ,
244+ geminiExtensionManifest (
245+ metadata ,
246+ pluginConfig . version ?? version ,
247+ pluginName ,
248+ pluginConfig ,
249+ mcp ,
250+ ) ,
230251 mcp : "inline" ,
231252 } ) ;
232253
@@ -239,7 +260,7 @@ async function emitCopilot(
239260 targetConfig : TargetConfig ,
240261 outDir : string ,
241262) : Promise < Artifact > {
242- const version = project . config . version ;
263+ const version = targetConfig . version ?? project . config . version ;
243264 const pluginRoot = targetConfig . pluginRoot ?? "plugins" ;
244265 const files = new Map < string , string | Buffer > ( ) ;
245266 const plugins : Record < string , unknown > [ ] = [ ] ;
@@ -279,24 +300,29 @@ async function emitCopilot(
279300 name : pluginName ,
280301 source : `./${ pluginPath } ` ,
281302 description : pluginConfig . description ?? metadata ?. description ,
282- version,
303+ version : pluginConfig . version ?? version ,
283304 skills,
284305 mcpServers : mcpServers ? ".mcp.json" : undefined ,
285306 } ) ,
286307 ) ;
287308 }
288309
289- const marketplace = stripUndefined ( {
290- name : project . config . name ,
291- metadata : stripUndefined ( {
292- description : project . config . metadata ?. description ,
293- version,
294- keywords : project . config . metadata ?. keywords ,
295- } ) ,
296- owner : project . config . metadata ?. owner ?? project . config . metadata ?. author ,
297- plugins,
298- ...targetConfig . manifest ,
299- } ) ;
310+ const marketplace = stripUndefined (
311+ deepMerge (
312+ {
313+ name : project . config . name ,
314+ metadata : stripUndefined ( {
315+ description : project . config . metadata ?. description ,
316+ version,
317+ keywords : project . config . metadata ?. keywords ,
318+ } ) ,
319+ owner :
320+ project . config . metadata ?. owner ?? project . config . metadata ?. author ,
321+ plugins,
322+ } ,
323+ targetConfig . manifest ?? { } ,
324+ ) ,
325+ ) ;
300326 // Copilot reuses the Claude marketplace schema and reads it from both the
301327 // repo-root .claude-plugin/ and .github/plugin/ (see github/copilot-plugins).
302328 const marketplaceJson = json ( marketplace ) ;
@@ -366,7 +392,7 @@ function cursorPluginManifest(
366392 if ( mcpServers ) {
367393 manifest . mcpServers = "./.mcp.json" ;
368394 }
369- return stripUndefined ( { ... manifest , ... pluginConfig . manifest } ) ;
395+ return stripUndefined ( deepMerge ( manifest , pluginConfig . manifest ?? { } ) ) ;
370396}
371397
372398function claudePluginManifest (
@@ -375,7 +401,7 @@ function claudePluginManifest(
375401 pluginName : string ,
376402 pluginConfig : EmittedPluginConfig ,
377403) : Record < string , unknown > {
378- return stripUndefined ( {
404+ const manifest : Record < string , unknown > = {
379405 name : pluginName ,
380406 version,
381407 description : pluginConfig . description ?? metadata ?. description ,
@@ -384,8 +410,8 @@ function claudePluginManifest(
384410 repository : metadata ?. repository ,
385411 license : metadata ?. license ,
386412 keywords : metadata ?. keywords ,
387- ... pluginConfig . manifest ,
388- } ) ;
413+ } ;
414+ return stripUndefined ( deepMerge ( manifest , pluginConfig . manifest ?? { } ) ) ;
389415}
390416
391417function geminiExtensionManifest (
@@ -395,13 +421,13 @@ function geminiExtensionManifest(
395421 pluginConfig : EmittedPluginConfig ,
396422 mcpServers : Record < string , unknown > | undefined ,
397423) : Record < string , unknown > {
398- return stripUndefined ( {
424+ const manifest : Record < string , unknown > = {
399425 name : pluginName ,
400426 version,
401427 description : pluginConfig . description ?? metadata ?. description ,
402428 mcpServers,
403- ... pluginConfig . manifest ,
404- } ) ;
429+ } ;
430+ return stripUndefined ( deepMerge ( manifest , pluginConfig . manifest ?? { } ) ) ;
405431}
406432
407433function artifact (
@@ -427,6 +453,30 @@ function stripUndefined<T extends Record<string, unknown>>(value: T): T {
427453 return value ;
428454}
429455
456+ function isPlainObject ( value : unknown ) : value is Record < string , unknown > {
457+ return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
458+ }
459+
460+ // Deep-merge an override onto a generated manifest. Nested objects merge so a
461+ // sibling key isn't lost; arrays and scalars from the override replace (not
462+ // concatenate, so keywords/tags don't double up). This is the general escape
463+ // hatch — any field, at any depth, can be overridden via a target/plugin
464+ // `manifest`.
465+ function deepMerge (
466+ base : Record < string , unknown > ,
467+ override : Record < string , unknown > ,
468+ ) : Record < string , unknown > {
469+ const result : Record < string , unknown > = { ...base } ;
470+ for ( const [ key , value ] of Object . entries ( override ) ) {
471+ const existing = result [ key ] ;
472+ result [ key ] =
473+ isPlainObject ( existing ) && isPlainObject ( value )
474+ ? deepMerge ( existing , value )
475+ : value ;
476+ }
477+ return result ;
478+ }
479+
430480function titleCase ( value : string ) : string {
431481 return value
432482 . split ( / [ - _ . ] / )
0 commit comments