@@ -147,31 +147,6 @@ 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- const skillsDir = join ( cwd , DIR_CURSOR , DIR_SKILLS ) ;
154- try {
155- const entries = await readdir ( skillsDir , { withFileTypes : true } ) ;
156- const flattenedPrefix = `aipm-${ marketplaceName . replace ( / \/ / g, '-' ) } -${ pluginName } ` ;
157- for ( const entry of entries ) {
158- if ( entry . isDirectory ( ) && entry . name . startsWith ( flattenedPrefix ) ) {
159- try {
160- await rm ( join ( skillsDir , entry . name ) , { recursive : true } ) ;
161- deletedCount ++ ;
162- } catch ( error ) {
163- if ( ! isFileNotFoundError ( error ) ) {
164- throw error ;
165- }
166- }
167- }
168- }
169- } catch ( error ) {
170- // Ignore errors reading skills directory
171- if ( ! isFileNotFoundError ( error ) ) {
172- throw error ;
173- }
174- }
175150 }
176151 } else {
177152 // Marketplace config missing - can still delete AIPM plugins, but not Claude Code meta-plugins
@@ -199,6 +174,61 @@ export async function pluginUninstall(options: unknown): Promise<void> {
199174 }
200175 }
201176
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+
202232 if ( deletedCount > 0 ) {
203233 defaultIO . logSuccess ( `Deleted plugin files from ${ deletedCount } location(s) in .cursor/` ) ;
204234 }
0 commit comments