@@ -147,6 +147,77 @@ export async function pluginUninstall(options: unknown): Promise<void> {
147147 }
148148 }
149149 }
150+
151+ // Remove flattened skill directories for this plugin
152+ // Pattern: .cursor/skills/aipm-<marketplace>-<plugin>[-subskill]
153+ // Must avoid prefix collisions when plugin names are prefixes of others
154+ // (e.g., "my-plugin" shouldn't match "my-plugin-extra")
155+ const skillsDir = join ( cwd , DIR_CURSOR , DIR_SKILLS ) ;
156+ try {
157+ const entries = await readdir ( skillsDir , { withFileTypes : true } ) ;
158+ const flattenedPrefix = `aipm-${ marketplaceName . replace ( / \/ / g, '-' ) } -${ pluginName } ` ;
159+
160+ // Check for conflicting plugin names in this marketplace
161+ const otherPluginsInMarketplace = Object . keys ( config . plugins ) . filter (
162+ ( id ) => id . endsWith ( `@${ marketplaceName } ` ) && id !== cmd . pluginId ,
163+ ) ;
164+
165+ const hasConflictingNames = otherPluginsInMarketplace . some ( ( id ) => {
166+ const otherPluginName = id . substring ( 0 , id . lastIndexOf ( '@' ) ) ;
167+ return otherPluginName . startsWith ( pluginName ) && otherPluginName !== pluginName ;
168+ } ) ;
169+
170+ // When conflicts exist, we need exact skill information to avoid false positives
171+ // Read the plugin's skills directory to get the exact list
172+ const skillsToDelete = new Set < string > ( ) ;
173+
174+ if ( hasConflictingNames && pluginPath ) {
175+ // Load exact skill information from plugin directory
176+ const skillsSourcePath = join ( pluginPath , 'skills' ) ;
177+ try {
178+ const skillEntries = await readdir ( skillsSourcePath , { withFileTypes : true } ) ;
179+ for ( const skillEntry of skillEntries ) {
180+ if ( skillEntry . isDirectory ( ) ) {
181+ // Reconstruct the exact flattened name for this skill
182+ const flatSkillName = `${ flattenedPrefix } -${ skillEntry . name } ` ;
183+ skillsToDelete . add ( flatSkillName ) ;
184+ }
185+ }
186+ } catch {
187+ // No skills directory - plugin has no skills
188+ }
189+ }
190+
191+ for ( const entry of entries ) {
192+ if ( ! entry . isDirectory ( ) ) continue ;
193+
194+ let shouldDelete = false ;
195+
196+ if ( hasConflictingNames ) {
197+ // With conflicts, only delete skills we explicitly found in the plugin directory
198+ shouldDelete = skillsToDelete . has ( entry . name ) ;
199+ } else {
200+ // No conflicts - safe to use prefix matching
201+ shouldDelete = entry . name === flattenedPrefix || entry . name . startsWith ( flattenedPrefix + '-' ) ;
202+ }
203+
204+ if ( shouldDelete ) {
205+ try {
206+ await rm ( join ( skillsDir , entry . name ) , { recursive : true } ) ;
207+ deletedCount ++ ;
208+ } catch ( error ) {
209+ if ( ! isFileNotFoundError ( error ) ) {
210+ throw error ;
211+ }
212+ }
213+ }
214+ }
215+ } catch ( error ) {
216+ // Ignore errors reading skills directory
217+ if ( ! isFileNotFoundError ( error ) ) {
218+ throw error ;
219+ }
220+ }
150221 }
151222 } else {
152223 // Marketplace config missing - can still delete AIPM plugins, but not Claude Code meta-plugins
@@ -174,61 +245,6 @@ export async function pluginUninstall(options: unknown): Promise<void> {
174245 }
175246 }
176247
177- // Remove flattened skill directories for this plugin
178- // Pattern: .cursor/skills/aipm-<marketplace>-<plugin>[-subskill]
179- // Must avoid prefix collisions when plugin names are prefixes of others
180- // (e.g., "my-plugin" shouldn't match "my-plugin-extra")
181- // Only safe to delete when we have accurate skill information from the manifest
182- if ( marketplaceConfig ) {
183- const skillsDir = join ( cwd , DIR_CURSOR , DIR_SKILLS ) ;
184- try {
185- const entries = await readdir ( skillsDir , { withFileTypes : true } ) ;
186- const flattenedPrefix = `aipm-${ marketplaceName . replace ( / \/ / g, '-' ) } -${ pluginName } ` ;
187-
188- // Only use prefix matching if no other plugins in this marketplace start with our plugin name
189- // This prevents accidentally deleting my-plugin-extra's skills when uninstalling my-plugin
190- const otherPluginsInMarketplace = Object . keys ( config . plugins ) . filter (
191- ( id ) => id . endsWith ( `@${ marketplaceName } ` ) && id !== cmd . pluginId ,
192- ) ;
193-
194- const hasConflictingNames = otherPluginsInMarketplace . some ( ( id ) => {
195- const otherPluginName = id . substring ( 0 , id . lastIndexOf ( '@' ) ) ;
196- return otherPluginName . startsWith ( pluginName ) && otherPluginName !== pluginName ;
197- } ) ;
198-
199- for ( const entry of entries ) {
200- let shouldDelete = false ;
201-
202- if ( entry . isDirectory ( ) ) {
203- if ( hasConflictingNames ) {
204- // If there are plugins with conflicting names, only delete exact match for root directory
205- // This is safer than trying to pattern-match when names could be ambiguous
206- shouldDelete = entry . name === flattenedPrefix ;
207- } else {
208- // No conflicting names - safe to use prefix matching
209- shouldDelete = entry . name === flattenedPrefix || entry . name . startsWith ( flattenedPrefix + '-' ) ;
210- }
211- }
212-
213- if ( shouldDelete ) {
214- try {
215- await rm ( join ( skillsDir , entry . name ) , { recursive : true } ) ;
216- deletedCount ++ ;
217- } catch ( error ) {
218- if ( ! isFileNotFoundError ( error ) ) {
219- throw error ;
220- }
221- }
222- }
223- }
224- } catch ( error ) {
225- // Ignore errors reading skills directory
226- if ( ! isFileNotFoundError ( error ) ) {
227- throw error ;
228- }
229- }
230- }
231-
232248 if ( deletedCount > 0 ) {
233249 defaultIO . logSuccess ( `Deleted plugin files from ${ deletedCount } location(s) in .cursor/` ) ;
234250 }
0 commit comments