Skip to content

Commit cd77a3e

Browse files
authored
fix: prevent Claude marketplaces from leaking into project config (#35)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent f0e544d commit cd77a3e

23 files changed

+319
-620
lines changed

docs/explanation/architecture.md

Lines changed: 86 additions & 419 deletions
Large diffs are not rendered by default.

docs/how-to/create-marketplace.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ See [Working with Monorepos](./monorepo-marketplace.md) for details.
238238
Check access:
239239

240240
```bash
241-
git clone <repository-url>
241+
git clone {repository-url}
242242
```
243243

244244
### "Permission denied"

docs/how-to/debug-plugins.md

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ aipm sync
5050

5151
```bash
5252
# For git marketplaces, clone and inspect
53-
git clone <marketplace-url> /tmp/check-marketplace
53+
git clone {marketplace-url} /tmp/check-marketplace
5454
cat /tmp/check-marketplace/marketplace.json
5555
```
5656

@@ -89,8 +89,9 @@ aipm sync
8989
4. **Check plugin files**:
9090

9191
```bash
92-
# Files should exist in .cursor/marketplace/
93-
ls -la .cursor/marketplace/my-marketplace/my-plugin/
92+
# Files should exist in .cursor/{type}/aipm/
93+
ls -la .cursor/commands/aipm/my-marketplace/my-plugin/
94+
ls -la .cursor/rules/aipm/my-marketplace/my-plugin/
9495
```
9596

9697
5. **Restart Claude Code** (if using with Claude Code)
@@ -106,7 +107,7 @@ aipm sync
106107
1. **Test git access**:
107108

108109
```bash
109-
git clone <repository-url> /tmp/test-clone
110+
git clone {repository-url} /tmp/test-clone
110111
```
111112

112113
2. **Check SSH keys** (for SSH URLs):
@@ -125,7 +126,7 @@ aipm sync
125126

126127
4. **Check branch exists**:
127128
```bash
128-
git ls-remote <repository-url>
129+
git ls-remote {repository-url}
129130
```
130131

131132
---
@@ -189,7 +190,7 @@ aipm sync
189190

190191
3. **Check git repository is accessible**:
191192
```bash
192-
git ls-remote <repository-url>
193+
git ls-remote {repository-url}
193194
```
194195

195196
---
@@ -218,10 +219,10 @@ aipm sync
218219
3. **Don't use sudo**:
219220

220221
```bash
221-
# ? Don't do this
222+
# BAD - Don't do this
222223
sudo aipm install ...
223224

224-
# ? Do this
225+
# GOOD - Do this
225226
aipm install ...
226227
```
227228

@@ -278,19 +279,17 @@ dir "%LOCALAPPDATA%\aipm\cache\my-marketplace"
278279
### Check Synced Files
279280

280281
```bash
281-
# Project-level
282-
ls -la .cursor/marketplace/
282+
# Project-level (plugin files are split by type)
283+
ls -la .cursor/commands/aipm/
284+
ls -la .cursor/rules/aipm/
283285

284286
# Check specific plugin files
285-
find .cursor/marketplace/ -name "*.md"
287+
find .cursor/*/aipm/ -name "*.md"
286288
```
287289

288290
### Manual Validation
289291

290292
```bash
291-
# Check plugin.json is valid
292-
cat .cursor/marketplace/my-marketplace/my-plugin/.claude-plugin/plugin.json | jq .
293-
294293
# Check marketplace.json is valid
295294
# Linux/macOS
296295
cat ~/.cache/aipm/my-marketplace/marketplace.json | jq .

docs/how-to/installation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ cd aipm
114114
bun install
115115

116116
# Run directly without building
117-
bun run src/cli.ts <command>
117+
bun run src/cli.ts {command}
118118

119119
# Or use watch mode
120120
bun run dev

docs/reference/cli-commands.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ aipm init --global
6767

6868
### `sync`
6969

70-
Sync all enabled plugins from marketplaces to `.cursor/marketplace/`.
70+
Sync all enabled plugins from marketplaces to `.cursor/` (commands, rules, agents, skills, hooks).
7171

7272
**Usage**:
7373

@@ -95,7 +95,7 @@ aipm sync --dry-run
9595
1. Loads configuration (three-way merge)
9696
2. For each enabled plugin:
9797
- Resolves marketplace path (clones/pulls git repos)
98-
- Copies plugin files to `.cursor/marketplace/{marketplace}/{plugin}/`
98+
- Copies plugin files to `.cursor/{type}/aipm/{marketplace}/{plugin}/` (where type is commands, rules, agents, skills, or hooks)
9999
3. Reports success/failures
100100

101101
**Exit codes**:
@@ -528,11 +528,11 @@ aipm list
528528
**Example output**:
529529

530530
```
531-
? Marketplaces:
531+
Marketplaces:
532532
- team-plugins: git@github.com:acme/plugins.git
533533
- local: ./my-plugins
534534
535-
? Installed Plugins:
535+
Installed Plugins:
536536
- code-reviewer@team-plugins (enabled, v1.2.0)
537537
- test-gen@local (disabled, v1.0.0)
538538
```

src/commands/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function list(options: ListOptions = {}): Promise<void> {
3434
if (marketplaceCount > 0) {
3535
console.log('\n📦 Marketplaces:');
3636
for (const [name, marketplace] of Object.entries(config.marketplaces)) {
37-
const isClaudeMarketplace = name.startsWith('claude:');
37+
const isClaudeMarketplace = name.startsWith('claude/');
3838
const sourceLabel = isClaudeMarketplace ? '🤖 Claude Code (auto-discovered)' : marketplace.source;
3939

4040
console.log(` • ${name}`);

src/commands/plugin-disable.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import merge from 'lodash.merge';
12
import { z } from 'zod';
23
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
34
import { FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
4-
import { saveConfig } from '../helpers/aipm-config';
5+
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
56
import { defaultIO } from '../helpers/io';
67

78
const PluginDisableOptionsSchema = z.object({
@@ -37,23 +38,18 @@ export async function pluginDisable(options: unknown): Promise<void> {
3738
return;
3839
}
3940

40-
const updatedConfig = {
41-
...config,
42-
plugins: {
43-
...config.plugins,
44-
[cmd.pluginId]: {
45-
...config.plugins[cmd.pluginId],
46-
enabled: false,
47-
},
48-
},
49-
};
50-
5141
if (cmd.dryRun) {
5242
defaultIO.logInfo(`[DRY RUN] Would disable plugin '${cmd.pluginId}' in ${configName}`);
53-
} else {
54-
await saveConfig(cwd, updatedConfig, cmd.local);
55-
defaultIO.logSuccess(`Disabled plugin '${cmd.pluginId}' in ${configName}`);
43+
return;
5644
}
45+
46+
const targetConfig = await loadTargetConfig(cwd, cmd.local);
47+
const updatedConfig = merge({}, targetConfig, {
48+
plugins: { [cmd.pluginId]: { enabled: false } },
49+
});
50+
51+
await saveConfig(cwd, updatedConfig, cmd.local);
52+
defaultIO.logSuccess(`Disabled plugin '${cmd.pluginId}' in ${configName}`);
5753
} catch (error: unknown) {
5854
const message = error instanceof Error ? error.message : String(error);
5955
defaultIO.logError(`Failed to disable plugin: ${message}`);

src/commands/plugin-enable.ts

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import merge from 'lodash.merge';
12
import { z } from 'zod';
23
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
34
import { FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
4-
import { saveConfig } from '../helpers/aipm-config';
5+
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
56
import { defaultIO } from '../helpers/io';
67

78
const PluginEnableOptionsSchema = z.object({
@@ -27,24 +28,6 @@ export async function pluginEnable(options: unknown): Promise<void> {
2728

2829
if (!config.plugins[cmd.pluginId]) {
2930
defaultIO.logInfo(`Plugin '${cmd.pluginId}' not found, adding it as enabled`);
30-
31-
const updatedConfig = {
32-
...config,
33-
plugins: {
34-
...config.plugins,
35-
[cmd.pluginId]: {
36-
enabled: true,
37-
},
38-
},
39-
};
40-
41-
if (cmd.dryRun) {
42-
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
43-
} else {
44-
await saveConfig(cwd, updatedConfig, cmd.local);
45-
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
46-
}
47-
return;
4831
}
4932

5033
const plugin = config.plugins[cmd.pluginId];
@@ -54,23 +37,18 @@ export async function pluginEnable(options: unknown): Promise<void> {
5437
return;
5538
}
5639

57-
const updatedConfig = {
58-
...config,
59-
plugins: {
60-
...config.plugins,
61-
[cmd.pluginId]: {
62-
...config.plugins[cmd.pluginId],
63-
enabled: true,
64-
},
65-
},
66-
};
67-
6840
if (cmd.dryRun) {
6941
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
70-
} else {
71-
await saveConfig(cwd, updatedConfig, cmd.local);
72-
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
42+
return;
7343
}
44+
45+
const targetConfig = await loadTargetConfig(cwd, cmd.local);
46+
const updatedConfig = merge({}, targetConfig, {
47+
plugins: { [cmd.pluginId]: { enabled: true } },
48+
});
49+
50+
await saveConfig(cwd, updatedConfig, cmd.local);
51+
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
7452
} catch (error: unknown) {
7553
const message = error instanceof Error ? error.message : String(error);
7654
defaultIO.logError(`Failed to enable plugin: ${message}`);

src/commands/plugin-install.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import merge from 'lodash.merge';
12
import { join } from 'node:path';
23
import { z } from 'zod';
34
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
45
import { DIR_CURSOR, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
5-
import { saveConfig } from '../helpers/aipm-config';
6+
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
67
import { fileExists } from '../helpers/fs';
78
import { resolveMarketplacePath } from '../helpers/git';
89
import { defaultIO } from '../helpers/io';
@@ -100,22 +101,17 @@ export async function pluginInstall(options: unknown): Promise<void> {
100101
return;
101102
}
102103

103-
const updatedConfig = {
104-
...config,
105-
plugins: {
106-
...config.plugins,
107-
[cmd.pluginId]: {
108-
enabled: true,
109-
},
110-
},
111-
};
112-
113104
if (cmd.dryRun) {
114105
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
115106
defaultIO.logInfo(`[DRY RUN] Would sync ${cmd.pluginId} to .cursor/`);
116107
return;
117108
}
118109

110+
const targetConfig = await loadTargetConfig(cwd, cmd.local);
111+
const updatedConfig = merge({}, targetConfig, {
112+
plugins: { [cmd.pluginId]: { enabled: true } },
113+
});
114+
119115
await saveConfig(cwd, updatedConfig, cmd.local);
120116
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
121117

src/commands/plugin-uninstall.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { rm } from 'node:fs/promises';
22
import { join } from 'node:path';
33
import { z } from 'zod';
44
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
5-
import { DIR_CURSOR, DIR_MARKETPLACE, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
6-
import { saveConfig } from '../helpers/aipm-config';
5+
import { DIR_AIPM_NAMESPACE, DIR_CURSOR, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL, PLUGIN_SUBDIRS } from '../constants';
6+
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
77
import { fileExists } from '../helpers/fs';
88
import { defaultIO } from '../helpers/io';
99

@@ -29,38 +29,54 @@ export async function pluginUninstall(options: unknown): Promise<void> {
2929
throw error;
3030
}
3131
const configName = cmd.local ? getConfigPath(FILE_AIPM_CONFIG_LOCAL) : getConfigPath(FILE_AIPM_CONFIG);
32+
const targetConfig = await loadTargetConfig(cwd, cmd.local);
3233

33-
if (!config.plugins[cmd.pluginId]) {
34+
if (!targetConfig.plugins[cmd.pluginId]) {
35+
if (config.plugins[cmd.pluginId]) {
36+
const error = new Error(
37+
`Plugin '${cmd.pluginId}' is not in ${configName} (exists in merged config from another source)`,
38+
);
39+
defaultIO.logError(error.message);
40+
throw error;
41+
}
3442
const error = new Error(`Plugin '${cmd.pluginId}' is not installed`);
3543
defaultIO.logError(error.message);
3644
throw error;
3745
}
3846

39-
const { [cmd.pluginId]: _removed, ...remainingPlugins } = config.plugins;
40-
41-
const updatedConfig = {
42-
...config,
43-
plugins: remainingPlugins,
44-
};
45-
4647
if (cmd.dryRun) {
4748
defaultIO.logInfo(`[DRY RUN] Would remove plugin '${cmd.pluginId}' from ${configName}`);
4849
if (cmd.removeFiles) {
49-
defaultIO.logInfo('[DRY RUN] Would delete files from .cursor/marketplace/');
50+
defaultIO.logInfo('[DRY RUN] Would delete plugin files from .cursor/');
5051
}
5152
} else {
53+
const { [cmd.pluginId]: _removed, ...remainingPlugins } = targetConfig.plugins;
54+
55+
const updatedConfig = {
56+
...targetConfig,
57+
plugins: remainingPlugins,
58+
};
59+
5260
await saveConfig(cwd, updatedConfig, cmd.local);
5361
defaultIO.logSuccess(`Removed plugin '${cmd.pluginId}' from ${configName}`);
5462

5563
if (cmd.removeFiles) {
5664
const [pluginName, marketplaceName] = cmd.pluginId.split('@');
5765

5866
if (pluginName && marketplaceName) {
59-
const installedPath = join(cwd, DIR_CURSOR, DIR_MARKETPLACE, marketplaceName, pluginName);
67+
let deletedCount = 0;
68+
69+
for (const subdir of PLUGIN_SUBDIRS) {
70+
const installedPath = join(cwd, DIR_CURSOR, subdir, DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
71+
72+
if (await fileExists(installedPath)) {
73+
await rm(installedPath, { recursive: true, force: true });
74+
deletedCount++;
75+
}
76+
}
6077

61-
if (await fileExists(installedPath)) {
62-
await rm(installedPath, { recursive: true, force: true });
63-
defaultIO.logSuccess(`Deleted plugin files from .cursor/marketplace/${marketplaceName}/${pluginName}`);
78+
if (deletedCount > 0) {
79+
defaultIO.logSuccess(`Deleted plugin files from ${deletedCount} location(s) in .cursor/`);
6480
}
6581
}
6682
}

0 commit comments

Comments
 (0)