@@ -716,32 +716,45 @@ const marketplaceRegistered = async (
716716 : 'absent' ;
717717} ;
718718
719- /** Installed rows for other plugins from the same marketplace: the marketplace registration then stays. */
720- const otherPluginsFromMarketplace = async (
719+ /**
720+ * Installed rows that still depend on the marketplace once the `id`@`scope` copy
721+ * is gone: other plugins from the same marketplace, plus (Claude) the same plugin
722+ * installed at another scope, because `plugin marketplace remove` unregisters the
723+ * marketplace for every scope. A second `plugin list --json` that cannot be read
724+ * is `'unknown'`, never an empty list: a failed read is not proof that nothing
725+ * depends on the marketplace, so the caller retains it (fail-closed).
726+ */
727+ const marketplaceDependents = async (
721728 runner : InstallCommandRunner ,
722729 identity : PluginIdentity ,
723730 host : Exclude < InstallHost , 'cursor' > ,
724731 marketplace : string ,
725732 id : string ,
726- ) : Promise < readonly string [ ] > => {
733+ scope : InstallScope ,
734+ ) : Promise < readonly string [ ] | 'unknown' > => {
727735 try {
728736 const result = await runner . run ( host , [ 'plugin' , 'list' , '--json' ] , { cwd : identity . bundleRoot } ) ;
729- if ( result . code !== 0 ) return [ ] ;
737+ if ( result . code !== 0 ) return 'unknown' ;
730738 const document = JSON . parse ( result . stdout ) as unknown ;
731739 const rows = host === 'claude'
732740 ? document
733741 : typeof document === 'object' && document !== null ? ( document as { readonly installed ?: unknown } ) . installed : undefined ;
734- if ( ! Array . isArray ( rows ) ) return [ ] ;
735- const ids : string [ ] = [ ] ;
742+ if ( ! Array . isArray ( rows ) ) return 'unknown' ;
743+ const dependents : string [ ] = [ ] ;
736744 for ( const row of rows ) {
737745 if ( typeof row !== 'object' || row === null ) continue ;
738- const candidate = ( row as { readonly id ?: unknown ; readonly pluginId ?: unknown } ) ;
746+ const candidate = ( row as { readonly id ?: unknown ; readonly pluginId ?: unknown ; readonly scope ?: unknown } ) ;
739747 const rowId = host === 'claude' ? candidate . id : candidate . pluginId ;
740- if ( typeof rowId === 'string' && rowId !== id && rowId . endsWith ( `@${ marketplace } ` ) ) ids . push ( rowId ) ;
748+ if ( typeof rowId !== 'string' || ! rowId . endsWith ( `@${ marketplace } ` ) ) continue ;
749+ if ( rowId !== id ) {
750+ dependents . push ( rowId ) ;
751+ } else if ( host === 'claude' && typeof candidate . scope === 'string' && candidate . scope !== scope ) {
752+ dependents . push ( `${ rowId } (scope ${ candidate . scope } )` ) ;
753+ }
741754 }
742- return ids ;
755+ return dependents ;
743756 } catch {
744- return [ ] ;
757+ return 'unknown' ;
745758 }
746759} ;
747760
@@ -896,7 +909,10 @@ const uninstallPublicCli = async (
896909 const marketplaceState = marketplaceRegistration === undefined
897910 ? 'absent'
898911 : await marketplaceRegistered ( runner , identity , host , marketplace ) ;
899- const sharedBy = marketplaceState === 'absent' ? [ ] : await otherPluginsFromMarketplace ( runner , identity , host , marketplace , id ) ;
912+ const dependents = marketplaceState === 'absent'
913+ ? [ ]
914+ : await marketplaceDependents ( runner , identity , host , marketplace , id , scope ) ;
915+ const retainMarketplace = dependents === 'unknown' || dependents . length > 0 ;
900916 const planned = options . plan === true ;
901917 const data = await publicHostData ( host , policy , entry , hostRoot , id ) ;
902918 const registrations : UninstallRegistrationReport [ ] = [ ] ;
@@ -914,12 +930,16 @@ const uninstallPublicCli = async (
914930 ...marketplaceRegistration ,
915931 action : marketplaceState === 'absent'
916932 ? 'already-absent'
917- : sharedBy . length > 0 ? 'retained' : planned ? 'planned' : 'removed' ,
933+ : retainMarketplace ? 'retained' : planned ? 'planned' : 'removed' ,
918934 detail : marketplaceState === 'absent'
919935 ? `${ host } no longer lists marketplace ${ marketplace } .`
920- : sharedBy . length > 0
921- ? `Marketplace ${ marketplace } stays registered: ${ sharedBy . join ( ', ' ) } still install from it.`
922- : `\`${ host } ${ publicHostMarketplaceRemoveArguments ( marketplace ) . join ( ' ' ) } \`` ,
936+ : dependents === 'unknown'
937+ ? `Marketplace ${ marketplace } stays registered: \`${ host } plugin list --json\` could not be re-read to prove ` +
938+ 'nothing else installs from it, and `plugin marketplace remove` applies to every scope. Remove it by hand once ' +
939+ 'the inventory is readable.'
940+ : dependents . length > 0
941+ ? `Marketplace ${ marketplace } stays registered: ${ dependents . join ( ', ' ) } still install from it.`
942+ : `\`${ host } ${ publicHostMarketplaceRemoveArguments ( marketplace ) . join ( ' ' ) } \`` ,
923943 } ) ) ;
924944 }
925945 const files = receipt === undefined && ! await exists ( receiptPath ) ? [ ] : [ receiptPath ] ;
@@ -944,7 +964,7 @@ const uninstallPublicCli = async (
944964 if ( entry !== undefined ) {
945965 await runHostCommand ( runner , identity , host , publicHostUninstallArguments ( host , id , scope ) , 'removal' ) ;
946966 }
947- if ( marketplaceRegistration !== undefined && marketplaceState !== 'absent' && sharedBy . length === 0 ) {
967+ if ( marketplaceRegistration !== undefined && marketplaceState !== 'absent' && ! retainMarketplace ) {
948968 await runHostCommand ( runner , identity , host , publicHostMarketplaceRemoveArguments ( marketplace ) , 'removal' ) ;
949969 }
950970 const purged : string [ ] = [ ] ;
0 commit comments