Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
505 changes: 86 additions & 419 deletions docs/explanation/architecture.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/how-to/create-marketplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ See [Working with Monorepos](./monorepo-marketplace.md) for details.
Check access:

```bash
git clone <repository-url>
git clone {repository-url}
```

### "Permission denied"
Expand Down
27 changes: 13 additions & 14 deletions docs/how-to/debug-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ aipm sync

```bash
# For git marketplaces, clone and inspect
git clone <marketplace-url> /tmp/check-marketplace
git clone {marketplace-url} /tmp/check-marketplace
cat /tmp/check-marketplace/marketplace.json
```

Expand Down Expand Up @@ -89,8 +89,9 @@ aipm sync
4. **Check plugin files**:

```bash
# Files should exist in .cursor/marketplace/
ls -la .cursor/marketplace/my-marketplace/my-plugin/
# Files should exist in .cursor/{type}/aipm/
ls -la .cursor/commands/aipm/my-marketplace/my-plugin/
ls -la .cursor/rules/aipm/my-marketplace/my-plugin/
```

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

```bash
git clone <repository-url> /tmp/test-clone
git clone {repository-url} /tmp/test-clone
```

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

4. **Check branch exists**:
```bash
git ls-remote <repository-url>
git ls-remote {repository-url}
```

---
Expand Down Expand Up @@ -189,7 +190,7 @@ aipm sync

3. **Check git repository is accessible**:
```bash
git ls-remote <repository-url>
git ls-remote {repository-url}
```

---
Expand Down Expand Up @@ -218,10 +219,10 @@ aipm sync
3. **Don't use sudo**:

```bash
# ? Don't do this
# BAD - Don't do this
sudo aipm install ...

# ? Do this
# GOOD - Do this
aipm install ...
```

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

```bash
# Project-level
ls -la .cursor/marketplace/
# Project-level (plugin files are split by type)
ls -la .cursor/commands/aipm/
ls -la .cursor/rules/aipm/

# Check specific plugin files
find .cursor/marketplace/ -name "*.md"
find .cursor/*/aipm/ -name "*.md"
```

### Manual Validation

```bash
# Check plugin.json is valid
cat .cursor/marketplace/my-marketplace/my-plugin/.claude-plugin/plugin.json | jq .

# Check marketplace.json is valid
# Linux/macOS
cat ~/.cache/aipm/my-marketplace/marketplace.json | jq .
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ cd aipm
bun install

# Run directly without building
bun run src/cli.ts <command>
bun run src/cli.ts {command}

# Or use watch mode
bun run dev
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ aipm init --global

### `sync`

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

**Usage**:

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

**Exit codes**:
Expand Down Expand Up @@ -528,11 +528,11 @@ aipm list
**Example output**:

```
? Marketplaces:
Marketplaces:
- team-plugins: git@github.com:acme/plugins.git
- local: ./my-plugins

? Installed Plugins:
Installed Plugins:
- code-reviewer@team-plugins (enabled, v1.2.0)
- test-gen@local (disabled, v1.0.0)
```
Expand Down
2 changes: 1 addition & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function list(options: ListOptions = {}): Promise<void> {
if (marketplaceCount > 0) {
console.log('\n📦 Marketplaces:');
for (const [name, marketplace] of Object.entries(config.marketplaces)) {
const isClaudeMarketplace = name.startsWith('claude:');
const isClaudeMarketplace = name.startsWith('claude/');
const sourceLabel = isClaudeMarketplace ? '🤖 Claude Code (auto-discovered)' : marketplace.source;

console.log(` • ${name}`);
Expand Down
26 changes: 11 additions & 15 deletions src/commands/plugin-disable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import merge from 'lodash.merge';
import { z } from 'zod';
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
import { FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
import { saveConfig } from '../helpers/aipm-config';
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
import { defaultIO } from '../helpers/io';

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

const updatedConfig = {
...config,
plugins: {
...config.plugins,
[cmd.pluginId]: {
...config.plugins[cmd.pluginId],
enabled: false,
},
},
};

if (cmd.dryRun) {
defaultIO.logInfo(`[DRY RUN] Would disable plugin '${cmd.pluginId}' in ${configName}`);
} else {
await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Disabled plugin '${cmd.pluginId}' in ${configName}`);
return;
}

const targetConfig = await loadTargetConfig(cwd, cmd.local);
const updatedConfig = merge({}, targetConfig, {
plugins: { [cmd.pluginId]: { enabled: false } },
});

await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Disabled plugin '${cmd.pluginId}' in ${configName}`);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
defaultIO.logError(`Failed to disable plugin: ${message}`);
Expand Down
44 changes: 11 additions & 33 deletions src/commands/plugin-enable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import merge from 'lodash.merge';
import { z } from 'zod';
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
import { FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
import { saveConfig } from '../helpers/aipm-config';
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
import { defaultIO } from '../helpers/io';

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

if (!config.plugins[cmd.pluginId]) {
defaultIO.logInfo(`Plugin '${cmd.pluginId}' not found, adding it as enabled`);

const updatedConfig = {
...config,
plugins: {
...config.plugins,
[cmd.pluginId]: {
enabled: true,
},
},
};

if (cmd.dryRun) {
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
} else {
await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
}
return;
}

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

const updatedConfig = {
...config,
plugins: {
...config.plugins,
[cmd.pluginId]: {
...config.plugins[cmd.pluginId],
enabled: true,
},
},
};

if (cmd.dryRun) {
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
} else {
await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
return;
}

const targetConfig = await loadTargetConfig(cwd, cmd.local);
const updatedConfig = merge({}, targetConfig, {
plugins: { [cmd.pluginId]: { enabled: true } },
});

await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
defaultIO.logError(`Failed to enable plugin: ${message}`);
Expand Down
18 changes: 7 additions & 11 deletions src/commands/plugin-install.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import merge from 'lodash.merge';
import { join } from 'node:path';
import { z } from 'zod';
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
import { DIR_CURSOR, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
import { saveConfig } from '../helpers/aipm-config';
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
import { fileExists } from '../helpers/fs';
import { resolveMarketplacePath } from '../helpers/git';
import { defaultIO } from '../helpers/io';
Expand Down Expand Up @@ -100,22 +101,17 @@ export async function pluginInstall(options: unknown): Promise<void> {
return;
}

const updatedConfig = {
...config,
plugins: {
...config.plugins,
[cmd.pluginId]: {
enabled: true,
},
},
};

if (cmd.dryRun) {
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
defaultIO.logInfo(`[DRY RUN] Would sync ${cmd.pluginId} to .cursor/`);
return;
}

const targetConfig = await loadTargetConfig(cwd, cmd.local);
const updatedConfig = merge({}, targetConfig, {
plugins: { [cmd.pluginId]: { enabled: true } },
});

Comment thread
cursor[bot] marked this conversation as resolved.
await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);

Expand Down
46 changes: 31 additions & 15 deletions src/commands/plugin-uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { rm } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
import { DIR_CURSOR, DIR_MARKETPLACE, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
import { saveConfig } from '../helpers/aipm-config';
import { DIR_AIPM_NAMESPACE, DIR_CURSOR, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL, PLUGIN_SUBDIRS } from '../constants';
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
import { fileExists } from '../helpers/fs';
import { defaultIO } from '../helpers/io';

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

if (!config.plugins[cmd.pluginId]) {
if (!targetConfig.plugins[cmd.pluginId]) {
if (config.plugins[cmd.pluginId]) {
const error = new Error(
`Plugin '${cmd.pluginId}' is not in ${configName} (exists in merged config from another source)`,
);
defaultIO.logError(error.message);
throw error;
}
const error = new Error(`Plugin '${cmd.pluginId}' is not installed`);
defaultIO.logError(error.message);
throw error;
}

const { [cmd.pluginId]: _removed, ...remainingPlugins } = config.plugins;

const updatedConfig = {
...config,
plugins: remainingPlugins,
};

if (cmd.dryRun) {
defaultIO.logInfo(`[DRY RUN] Would remove plugin '${cmd.pluginId}' from ${configName}`);
if (cmd.removeFiles) {
defaultIO.logInfo('[DRY RUN] Would delete files from .cursor/marketplace/');
defaultIO.logInfo('[DRY RUN] Would delete plugin files from .cursor/');
Comment thread
yordis marked this conversation as resolved.
}
} else {
const { [cmd.pluginId]: _removed, ...remainingPlugins } = targetConfig.plugins;

const updatedConfig = {
...targetConfig,
plugins: remainingPlugins,
};

await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Removed plugin '${cmd.pluginId}' from ${configName}`);

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

if (pluginName && marketplaceName) {
const installedPath = join(cwd, DIR_CURSOR, DIR_MARKETPLACE, marketplaceName, pluginName);
let deletedCount = 0;

for (const subdir of PLUGIN_SUBDIRS) {
const installedPath = join(cwd, DIR_CURSOR, subdir, DIR_AIPM_NAMESPACE, marketplaceName, pluginName);

if (await fileExists(installedPath)) {
await rm(installedPath, { recursive: true, force: true });
deletedCount++;
}
}

if (await fileExists(installedPath)) {
await rm(installedPath, { recursive: true, force: true });
defaultIO.logSuccess(`Deleted plugin files from .cursor/marketplace/${marketplaceName}/${pluginName}`);
if (deletedCount > 0) {
defaultIO.logSuccess(`Deleted plugin files from ${deletedCount} location(s) in .cursor/`);
}
}
}
Expand Down
Loading