diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md
index 7914910..78b7bf7 100644
--- a/docs/explanation/architecture.md
+++ b/docs/explanation/architecture.md
@@ -1,473 +1,140 @@
# Architecture Overview
-This document explains how aipm is designed and how its components work together.
-
----
+aipm follows a **layered architecture** with clear separation of concerns.
## System Design
-aipm follows a **layered architecture** with clear separation of concerns:
-
-```
-???????????????????????????????????????????
-? CLI Layer (cli.ts) ? ? User interaction
-???????????????????????????????????????????
-? Commands Layer (commands/) ? ? Business logic
-???????????????????????????????????????????
-? Config Layer (config/) ? ? Configuration management
-???????????????????????????????????????????
-? Utils Layer (utils/) ? ? Shared utilities
-???????????????????????????????????????????
-? External Systems (Git, FS) ? ? Infrastructure
-???????????????????????????????????????????
-```
-
----
-
-## Core Components
-
-### 1. CLI Layer
-
-**Responsibility**: Parse arguments and route to commands
-
-**Key file**: `src/cli.ts`
-
-**What it does**:
-
-- Parses command-line arguments using Node's `parseArgs`
-- Routes to appropriate command handler
-- Handles `--help`, `--version`, global flags
-- Catches and displays errors
-
-**Example flow**:
-
-```
-$ aipm plugin install my-plugin@local
- ?
-cli.ts parses args ? { command: "plugin", subcommand: "install", pluginId: "my-plugin@local" }
- ?
-Calls: pluginInstall({ pluginId: "my-plugin@local", ... })
-```
-
----
-
-### 2. Commands Layer
-
-**Responsibility**: Implement business logic for each command
-
-**Location**: `src/commands/`
-
-**Key commands**:
-
-- `init.ts` - Initialize project configuration
-- `sync.ts` - Sync plugins from marketplaces
-- `marketplace-*.ts` - Marketplace management
-- `plugin-*.ts` - Plugin management
-
-**Common pattern**:
-
-```typescript
-export async function commandName(options: Options) {
- // 1. Validate options (using Zod)
- const validated = OptionsSchema.parse(options);
-
- // 2. Load config
- const config = await loadPluginsConfig();
-
- // 3. Perform operation
- // ... business logic ...
-
- // 4. Save config (if needed)
- await writeConfig(config);
-
- // 5. Provide feedback
- console.log('? Success!');
-}
-```
-
----
-
-### 3. Config Layer
-
-**Responsibility**: Load, merge, and validate configuration
-
-**Location**: `src/config/`
-
-**Key files**:
-
-- `loader.ts` - Three-way config merge
-- `schema.ts` - TypeScript types
-- `validation.ts` - Zod schemas
-
-**Three-way merge**:
-
-```typescript
-// Priority: local > project > global
-const config = mergeConfigs(
- globalConfig, // ~/.config/aipm/config.json (or %APPDATA%\aipm\config.json on Windows)
- projectConfig, // .aipm/config.json
- localConfig, // .aipm/config.local.json (gitignored)
-);
+```mermaid
+graph TD
+ A[CLI Layer
cli.ts] --> B[Commands Layer
commands/]
+ B --> C[Config Layer
config/]
+ B --> D[Helpers Layer
helpers/]
+ C --> E[External Systems
Git, FS]
+ D --> E
```
-See [Configuration System](./config-system.md) for details.
-
----
-
-### 4. Utils Layer
-
-**Responsibility**: Shared utilities and helpers
-
-**Location**: `src/utils/`
-
-**Key modules**:
-
-- `git.ts` - Git operations (clone, pull, cache)
-- `fs.ts` - File system operations
-- `marketplace.ts` - Marketplace resolution
-- `plugin.ts` - Plugin operations
-- `sync-strategy.ts` - Sync algorithm
-- `paths.ts` - Path resolution
-- `github.ts` - GitHub URL handling
-- `io.ts` - User input/output
+| Layer | Responsibility |
+| -------- | ----------------------------------- |
+| CLI | Parse arguments, route to commands |
+| Commands | Business logic for each command |
+| Config | Load, merge, validate configuration |
+| Helpers | Shared utilities (git, fs, sync) |
---
## Data Flow
-### Installing a Plugin
-
-```
-User: aipm plugin install my-plugin@local
- ?
-1. CLI parses command
- ?
-2. pluginInstall() called
- ?
-3. Load config (three-way merge)
- ?
-4. Parse pluginId ? { name: "my-plugin", marketplace: "local" }
- ?
-5. Check if plugin exists in marketplace
- ?
-6. Enable plugin in config
- ?
-7. Sync plugin files (via syncCommand)
- ?
-8. Resolve marketplace path (git clone/pull if needed)
- ?
-9. Copy plugin files to .cursor/marketplace/
- ?
-10. Save config
- ?
-11. Show success message
-```
+### Plugin Installation
-### Syncing Plugins
+```mermaid
+sequenceDiagram
+ participant User
+ participant CLI
+ participant Config
+ participant Marketplace
+ participant FS
+ User->>CLI: aipm plugin install my-plugin@local
+ CLI->>Config: Load config (three-way merge)
+ CLI->>Marketplace: Resolve marketplace path
+ Marketplace-->>CLI: Clone/pull if git, or use local path
+ CLI->>FS: Copy files to .cursor/{type}/aipm/
+ CLI->>Config: Save updated config
+ CLI->>User: Success message
```
-User: aipm sync
- ?
-1. Load config
- ?
-2. For each enabled plugin:
- a. Resolve marketplace path
- - Git: Clone to ~/.cache/aipm/ (or pull if exists)
- - Local: Use directory directly
- - URL: Fetch marketplace.json
- b. Find plugin source in marketplace
- c. Copy plugin files to .cursor/marketplace/{marketplace}/{plugin}/
- ?
-3. Show summary
-```
-
----
-
-## Configuration Management
-### File Locations
+### Sync Flow
-```
-User's machine:
-└ ~/.config/aipm/ (config)
-└ ~/.cache/aipm/ (cache)
- ├── config.json # Global config
- └── cache/
- └── {marketplace}/ # Git marketplace clones
-
-Project:
-└ .aipm/
- ├── config.json # Project config (committed)
- └── config.local.json # Local overrides (gitignored)
-
-└ .cursor/ # Synced plugin content
- └── marketplace/
- └── {marketplace}/
- └── {plugin}/ # Plugin files
-```
+```mermaid
+sequenceDiagram
+ participant User
+ participant CLI
+ participant Config
+ participant FS
-### Config Priority
-
-```
-Local (.aipm/config.local.json)
- ? overrides
-Project (.aipm/config.json)
- ? overrides
-Global (~/.config/aipm/config.json)
+ User->>CLI: aipm sync
+ CLI->>Config: Load enabled plugins
+ loop Each enabled plugin
+ CLI->>FS: Copy to .cursor/{type}/aipm/{marketplace}/{plugin}/
+ end
+ CLI->>User: Summary
```
-See [Configuration System](./config-system.md) for details.
-
---
-## Marketplace Resolution
-
-Different marketplace types are resolved differently:
-
-### Local Directory
+## File Locations
```
-marketplace: { source: "directory", path: "./my-plugins" }
- ?
-Resolve to absolute path
- ?
-Scan directory for plugins
-```
+~/.config/aipm/config.json # Global config
+~/.cache/aipm/{marketplace}/ # Git marketplace clones
-### Git Repository
+.aipm/
+├── config.json # Project config (committed)
+└── config.local.json # Local overrides (gitignored)
-```
-marketplace: { source: "git", url: "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/user/plugins.git" }
- ?
-Clone to ~/.cache/aipm/{marketplace-name}/
- (or pull if already exists)
- ?
-Read marketplace.json from clone
- ?
-Scan for plugins
+.cursor/
+├── commands/aipm/{marketplace}/{plugin}/
+├── rules/aipm/{marketplace}/{plugin}/
+├── agents/aipm/{marketplace}/{plugin}/
+├── skills/aipm/{marketplace}/{plugin}/
+└── hooks/aipm/{marketplace}/{plugin}/
```
-### Remote URL
-
-```
-marketplace: { source: "url", url: "https://cdn.com/marketplace.json" }
- ?
-Fetch marketplace.json
- ?
-Cache to ~/.cache/aipm/{marketplace-name}/
- ?
-Parse plugin list
-```
-
-See [Marketplace Types](./marketplace-types.md) for details.
-
---
-## Plugin Sync Strategy
-
-aipm uses a **copy-based sync strategy**:
-
-1. **Resolve source**: Find where the plugin lives (marketplace path)
-2. **Copy files**: Copy entire plugin directory to `.cursor/marketplace/`
-3. **Preserve structure**: Maintain plugin directory structure
+## Config Priority
-**Why copy instead of symlink?**
-
-- Cross-platform compatibility (Windows support)
-- No broken links if source moves
-- Isolated per-project (different projects can have different versions)
-
-See [Git Cache Strategy](./git-cache.md) for git-specific details.
-
----
-
-## Error Handling
-
-aipm uses a **fail-fast** approach:
-
-```typescript
-// Validate early
-const options = OptionsSchema.parse(input); // Throws if invalid
-
-// Check preconditions
-if (!configExists) {
- throw new Error("Run 'aipm init' first");
-}
-
-// Atomic operations where possible
-// Either complete fully or rollback
-```
-
-**Error bubbling**:
-
-```
-Utils layer throws
- ?
-Commands layer catches, adds context
- ?
-CLI layer catches, formats for user
- ?
-process.exit(1)
+```mermaid
+graph TD
+ A[Local: .aipm/config.local.json] -->|overrides| B[Project: .aipm/config.json]
+ B -->|overrides| C[Global: ~/.config/aipm/config.json]
```
---
-## Testing Strategy
-
-aipm uses **test doubles** for external dependencies:
+## Marketplace Resolution
-```typescript
-// Mock file system
-const mockFS = {
- readFile: async () => 'mock content',
- writeFile: async () => {},
-};
+```mermaid
+graph LR
+ subgraph Sources
+ A[Local Directory]
+ B[Git Repository]
+ C[Remote URL]
+ end
-// Mock IO
-const mockIO = new MockIO();
-mockIO.confirmResponses = [true]; // Auto-confirm prompts
+ A --> D[Use path directly]
+ B --> E[Clone to ~/.cache/aipm/]
+ C --> F[Fetch & cache marketplace.json]
-// Test commands in isolation
-await command({ io: mockIO, fs: mockFS });
+ D --> G[Scan for plugins]
+ E --> G
+ F --> G
```
-See [Testing Guide](../../TESTING.md) for details.
-
---
## Design Principles
-### 1. **Explicit over implicit**
-
-- Config files are explicit (not hidden magic)
-- All paths are absolute when resolved
-- Clear error messages
-
-### 2. **Composable**
-
-- Small, focused functions
-- Utilities can be used independently
-- Commands build on utils
-
-### 3. **Type-safe**
-
-- TypeScript strict mode
-- Zod runtime validation
-- Catch errors at boundaries
-
-### 4. **Testable**
-
-- Dependency injection (IO, FS)
-- Pure functions where possible
-- Integration tests for workflows
-
-### 5. **User-friendly**
-
-- `--dry-run` for safety
-- Helpful error messages
-- Progress feedback
+1. **Explicit over implicit** - Config files are explicit, paths are absolute
+2. **Composable** - Small functions, utilities work independently
+3. **Type-safe** - TypeScript strict mode, Zod validation
+4. **Testable** - Dependency injection, pure functions
+5. **User-friendly** - `--dry-run`, helpful errors
---
## Extension Points
-aipm is designed to be extended:
-
-### Adding New Commands
-
-1. Create `src/commands/my-command.ts`
-2. Define options schema in `src/config/validation.ts`
-3. Register in `src/cli.ts`
-4. Add tests in `tests/commands/my-command.test.ts`
-
-### Adding New Marketplace Types
-
-1. Add type to `MarketplaceSource` in `src/config/schema.ts`
-2. Update `resolveMarketplacePath()` in `src/utils/git.ts`
-3. Add validation in `src/config/validation.ts`
-4. Add tests
-
-### Adding New Sync Strategies
-
-1. Implement strategy in `src/utils/sync-strategy.ts`
-2. Add configuration option
-3. Update `syncCommand()` to use new strategy
-
----
-
-## Performance Considerations
-
-### Git Operations
-
-- **Shallow clones**: Uses `--depth 1` for faster clones
-- **Caching**: Git repos cached in `~/.cache/aipm/` (follows XDG Base Directory spec)
-- **Lazy updates**: Only pulls when `marketplace update` called
-
-### File Operations
-
-- **Batch copies**: Uses efficient file copying
-- **Concurrent ops**: Multiple plugins synced in parallel (future)
-- **Skip unchanged**: Only copies if source changed (future)
-
-### Configuration
-
-- **Lazy loading**: Config only loaded when needed
-- **Minimal parsing**: Uses streaming JSON where possible
-- **No watchers**: Explicit sync, no file watching overhead
-
----
-
-## Security Considerations
-
-### Git Clones
-
-- **Force reset**: Always reset to remote state (prevent tampering)
-- **Clean untracked**: Remove untracked files on update
-- **No auto-exec**: Never execute code from plugins during sync
-
-### File Paths
-
-- **Path traversal protection**: Validate all paths
-- **Absolute paths**: Resolve to absolute to prevent confusion
-- **No hidden writes**: All file operations logged
-
-### Configuration
-
-- **Local overrides**: Secrets go in `.aipm/config.local.json` (gitignored)
-- **Validation**: All configs validated with Zod schemas
-- **No eval**: No dynamic code execution
-
-See [Security Policy](../../SECURITY.md) for more.
-
----
-
-## Future Architecture
-
-Potential improvements:
-
-### Plugin API
-
-- Plugins could define TypeScript APIs
-- Version compatibility checking
-- Dependency resolution between plugins
-
-### Remote Registry
-
-- Central plugin registry (like npm)
-- Publishing workflow
-- Automatic updates
-
-### Performance
-
-- Parallel sync operations
-- Incremental updates (only changed files)
-- Content-addressable storage (like git)
+| Extension | Steps |
+| -------------------- | ------------------------------------------------------------- |
+| New command | Create `src/commands/my-command.ts`, register in `cli.ts` |
+| New marketplace type | Add to `MarketplaceSource`, update `resolveMarketplacePath()` |
+| New sync strategy | Implement in `sync-strategy.ts`, add config option |
---
## Related
-- [Configuration System](./config-system.md) - Config details
-- [Git Cache Strategy](./git-cache.md) - Git operations
-- [Plugin Lifecycle](./plugin-lifecycle.md) - Plugin states
-- [Design Decisions](./design-decisions.md) - Why we made these choices
+- [Configuration System](./config-system.md)
+- [Git Cache Strategy](./git-cache.md)
+- [Plugin Lifecycle](./plugin-lifecycle.md)
diff --git a/docs/how-to/create-marketplace.md b/docs/how-to/create-marketplace.md
index db88c92..9f60eca 100644
--- a/docs/how-to/create-marketplace.md
+++ b/docs/how-to/create-marketplace.md
@@ -238,7 +238,7 @@ See [Working with Monorepos](./monorepo-marketplace.md) for details.
Check access:
```bash
-git clone
+git clone {repository-url}
```
### "Permission denied"
diff --git a/docs/how-to/debug-plugins.md b/docs/how-to/debug-plugins.md
index b88a72e..7487830 100644
--- a/docs/how-to/debug-plugins.md
+++ b/docs/how-to/debug-plugins.md
@@ -50,7 +50,7 @@ aipm sync
```bash
# For git marketplaces, clone and inspect
- git clone /tmp/check-marketplace
+ git clone {marketplace-url} /tmp/check-marketplace
cat /tmp/check-marketplace/marketplace.json
```
@@ -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)
@@ -106,7 +107,7 @@ aipm sync
1. **Test git access**:
```bash
- git clone /tmp/test-clone
+ git clone {repository-url} /tmp/test-clone
```
2. **Check SSH keys** (for SSH URLs):
@@ -125,7 +126,7 @@ aipm sync
4. **Check branch exists**:
```bash
- git ls-remote
+ git ls-remote {repository-url}
```
---
@@ -189,7 +190,7 @@ aipm sync
3. **Check git repository is accessible**:
```bash
- git ls-remote
+ git ls-remote {repository-url}
```
---
@@ -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 ...
```
@@ -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 .
diff --git a/docs/how-to/installation.md b/docs/how-to/installation.md
index 68aa4db..ac372ae 100644
--- a/docs/how-to/installation.md
+++ b/docs/how-to/installation.md
@@ -114,7 +114,7 @@ cd aipm
bun install
# Run directly without building
-bun run src/cli.ts
+bun run src/cli.ts {command}
# Or use watch mode
bun run dev
diff --git a/docs/reference/cli-commands.md b/docs/reference/cli-commands.md
index 5ab6d38..107845e 100644
--- a/docs/reference/cli-commands.md
+++ b/docs/reference/cli-commands.md
@@ -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**:
@@ -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**:
@@ -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)
```
diff --git a/src/commands/list.ts b/src/commands/list.ts
index 7c054fb..f1c58ff 100644
--- a/src/commands/list.ts
+++ b/src/commands/list.ts
@@ -34,7 +34,7 @@ export async function list(options: ListOptions = {}): Promise {
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}`);
diff --git a/src/commands/plugin-disable.ts b/src/commands/plugin-disable.ts
index ac2b6b7..e45c69b 100644
--- a/src/commands/plugin-disable.ts
+++ b/src/commands/plugin-disable.ts
@@ -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({
@@ -37,23 +38,18 @@ export async function pluginDisable(options: unknown): Promise {
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}`);
diff --git a/src/commands/plugin-enable.ts b/src/commands/plugin-enable.ts
index 5c90d71..48333ed 100644
--- a/src/commands/plugin-enable.ts
+++ b/src/commands/plugin-enable.ts
@@ -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({
@@ -27,24 +28,6 @@ export async function pluginEnable(options: unknown): Promise {
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];
@@ -54,23 +37,18 @@ export async function pluginEnable(options: unknown): Promise {
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}`);
diff --git a/src/commands/plugin-install.ts b/src/commands/plugin-install.ts
index 1360932..8c113e0 100644
--- a/src/commands/plugin-install.ts
+++ b/src/commands/plugin-install.ts
@@ -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';
@@ -100,22 +101,17 @@ export async function pluginInstall(options: unknown): Promise {
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 } },
+ });
+
await saveConfig(cwd, updatedConfig, cmd.local);
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
diff --git a/src/commands/plugin-uninstall.ts b/src/commands/plugin-uninstall.ts
index a6e7316..22dbf1d 100644
--- a/src/commands/plugin-uninstall.ts
+++ b/src/commands/plugin-uninstall.ts
@@ -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';
@@ -29,26 +29,34 @@ export async function pluginUninstall(options: unknown): Promise {
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/');
}
} 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}`);
@@ -56,11 +64,19 @@ export async function pluginUninstall(options: unknown): Promise {
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/`);
}
}
}
diff --git a/src/commands/sync.ts b/src/commands/sync.ts
index 08e5f83..2e2c1e5 100644
--- a/src/commands/sync.ts
+++ b/src/commands/sync.ts
@@ -2,7 +2,7 @@ import { rm, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
-import { DIR_MARKETPLACE, PLUGIN_SUBDIRS } from '../constants';
+import { DIR_AIPM_NAMESPACE, DIR_MARKETPLACE, PLUGIN_SUBDIRS } from '../constants';
import { ensureDir, fileExists } from '../helpers/fs';
import { resolveMarketplacePath } from '../helpers/git';
import { defaultIO } from '../helpers/io';
@@ -81,13 +81,14 @@ export async function sync(options: SyncOptions = {}): Promise {
await rm(oldMarketplaceDir, { recursive: true, force: true });
}
- // Clear and recreate target directories to remove disabled plugins
+ // Clear and recreate aipm subdirectories to remove disabled plugins
+ // Only clears .cursor//aipm/, preserving user's own files
for (const subdir of targetSubdirs) {
- const subdirPath = join(targetDir, subdir);
- if (await fileExists(subdirPath)) {
- await rm(subdirPath, { recursive: true, force: true });
+ const aipmSubdirPath = join(targetDir, subdir, DIR_AIPM_NAMESPACE);
+ if (await fileExists(aipmSubdirPath)) {
+ await rm(aipmSubdirPath, { recursive: true, force: true });
}
- await ensureDir(subdirPath);
+ await ensureDir(aipmSubdirPath);
}
}
@@ -165,7 +166,7 @@ export async function sync(options: SyncOptions = {}): Promise {
const disabledSubdirs = PLUGIN_SUBDIRS.filter((subdir) => !isSubdirEnabled(subdir, includeConfig));
for (const subdir of disabledSubdirs) {
- const subdirPath = join(targetDir, subdir, marketplaceName, pluginName);
+ const subdirPath = join(targetDir, subdir, DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
if (await fileExists(subdirPath)) {
await rm(subdirPath, { recursive: true, force: true });
}
diff --git a/src/config/loader.ts b/src/config/loader.ts
index dd62340..588e9d3 100644
--- a/src/config/loader.ts
+++ b/src/config/loader.ts
@@ -105,7 +105,7 @@ export async function loadPluginsConfig(baseDir: string): Promise {
diff --git a/src/helpers/sync-strategy.ts b/src/helpers/sync-strategy.ts
index cee0afc..f06b66c 100644
--- a/src/helpers/sync-strategy.ts
+++ b/src/helpers/sync-strategy.ts
@@ -1,5 +1,6 @@
import { cp, readdir, readFile, writeFile } from 'node:fs/promises';
import { basename, extname, join } from 'node:path';
+import { DIR_AIPM_NAMESPACE } from '../constants';
import { DirectoryNotFoundError, isFileNotFoundError } from '../errors';
import { applyCursorFrontmatter } from './frontmatter';
import { ensureDir, fileExists } from './fs';
@@ -14,11 +15,11 @@ export type SyncResult = {
/**
* Syncs a plugin to the correct Cursor directories:
- * - commands/*.md → .cursor/commands/marketplace-name/plugin-name/
- * - rules/*.mdc → .cursor/rules/marketplace-name/plugin-name/
- * - agents/*.md → .cursor/agents/marketplace-name/plugin-name/
- * - skills/*.md → .cursor/skills/marketplace-name/plugin-name/
- * - hooks/* → .cursor/hooks/marketplace-name/plugin-name/
+ * - commands/*.md → .cursor/commands/aipm/marketplace-name/plugin-name/
+ * - rules/*.mdc → .cursor/rules/aipm/marketplace-name/plugin-name/
+ * - agents/*.md → .cursor/agents/aipm/marketplace-name/plugin-name/
+ * - skills/*.md → .cursor/skills/aipm/marketplace-name/plugin-name/
+ * - hooks/* → .cursor/hooks/aipm/marketplace-name/plugin-name/
*/
export async function syncPluginToCursor(
pluginPath: string,
@@ -34,42 +35,42 @@ export async function syncPluginToCursor(
hooksCount: 0,
};
- // Sync commands/*.md to .cursor/commands/marketplace/plugin/
+ // Sync commands/*.md to .cursor/commands/aipm/marketplace/plugin/
const commandsResult = await syncDirectory(
join(pluginPath, 'commands'),
- join(cursorDir, 'commands', marketplaceName, pluginName),
+ join(cursorDir, 'commands', DIR_AIPM_NAMESPACE, marketplaceName, pluginName),
['.md'],
);
result.commandsCount = commandsResult;
- // Sync rules/*.md and *.mdc to .cursor/rules/marketplace/plugin/
+ // Sync rules/*.md and *.mdc to .cursor/rules/aipm/marketplace/plugin/
// Apply .cursor.yaml frontmatter overrides if they exist
const rulesResult = await syncRulesDirectory(
join(pluginPath, 'rules'),
- join(cursorDir, 'rules', marketplaceName, pluginName),
+ join(cursorDir, 'rules', DIR_AIPM_NAMESPACE, marketplaceName, pluginName),
);
result.rulesCount = rulesResult;
- // Sync agents/*.md to .cursor/agents/marketplace/plugin/
+ // Sync agents/*.md to .cursor/agents/aipm/marketplace/plugin/
const agentsResult = await syncDirectory(
join(pluginPath, 'agents'),
- join(cursorDir, 'agents', marketplaceName, pluginName),
+ join(cursorDir, 'agents', DIR_AIPM_NAMESPACE, marketplaceName, pluginName),
['.md'],
);
result.agentsCount = agentsResult;
- // Sync skills/*.md to .cursor/skills/marketplace/plugin/
+ // Sync skills/*.md to .cursor/skills/aipm/marketplace/plugin/
const skillsResult = await syncDirectory(
join(pluginPath, 'skills'),
- join(cursorDir, 'skills', marketplaceName, pluginName),
+ join(cursorDir, 'skills', DIR_AIPM_NAMESPACE, marketplaceName, pluginName),
['.md'],
);
result.skillsCount = skillsResult;
- // Sync hooks/* to .cursor/hooks/marketplace/plugin/
+ // Sync hooks/* to .cursor/hooks/aipm/marketplace/plugin/
const hooksResult = await syncDirectory(
join(pluginPath, 'hooks'),
- join(cursorDir, 'hooks', marketplaceName, pluginName),
+ join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName),
[], // Copy all files in hooks
);
result.hooksCount = hooksResult;
diff --git a/tests/commands/claude-code-auto-discovery.test.ts b/tests/commands/claude-code-auto-discovery.test.ts
index ea47b04..d13bacf 100644
--- a/tests/commands/claude-code-auto-discovery.test.ts
+++ b/tests/commands/claude-code-auto-discovery.test.ts
@@ -44,9 +44,9 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
- expect(config.marketplaces['claude:anthropic-agent-skills']?.source).toBe('directory');
- expect(config.marketplaces['claude:anthropic-agent-skills']?.path).toContain('anthropic-agent-skills');
- expect(config.marketplaces['claude:claude-code-workflows']).toBeDefined();
+ expect(config.marketplaces['claude/anthropic-agent-skills']?.source).toBe('directory');
+ expect(config.marketplaces['claude/anthropic-agent-skills']?.path).toContain('anthropic-agent-skills');
+ expect(config.marketplaces['claude/claude-code-workflows']).toBeDefined();
});
test('Claude Code marketplaces appear in aipm list command', async () => {
@@ -100,7 +100,7 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
- expect(config.marketplaces['claude:test-marketplace']?.source).toBe('directory');
+ expect(config.marketplaces['claude/test-marketplace']?.source).toBe('directory');
});
test('does not interfere with manually configured AIPM marketplaces', async () => {
@@ -134,7 +134,7 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
expect(config.marketplaces['my-marketplace']).toBeDefined();
- expect(config.marketplaces['claude:claude-marketplace']).toBeDefined();
+ expect(config.marketplaces['claude/claude-marketplace']).toBeDefined();
});
test('handles missing Claude Code installation gracefully', async () => {
@@ -143,7 +143,7 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
expect(config).not.toBeNull();
- const claudeMarketplaces = Object.keys(config.marketplaces || {}).filter((name) => name.startsWith('claude:'));
+ const claudeMarketplaces = Object.keys(config.marketplaces || {}).filter((name) => name.startsWith('claude/'));
expect(claudeMarketplaces).toHaveLength(0);
});
@@ -165,7 +165,7 @@ describe('Claude Code Auto-Discovery Integration', () => {
const configPath = join(testDir, '.aipm', 'config.json');
const aipmConfig = {
marketplaces: {
- 'claude:my-marketplace': {
+ 'claude/my-marketplace': {
source: 'directory',
path: './aipm-marketplace',
},
@@ -177,7 +177,7 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
- expect(config.marketplaces['claude:my-marketplace']?.path).toBe('./aipm-marketplace');
+ expect(config.marketplaces['claude/my-marketplace']?.path).toBe('./aipm-marketplace');
});
test('discovers git-based Claude Code marketplace', async () => {
@@ -198,8 +198,8 @@ describe('Claude Code Auto-Discovery Integration', () => {
const { config } = await loadPluginsConfig(testDir);
- expect(config.marketplaces['claude:git-marketplace']?.source).toBe('git');
- expect(config.marketplaces['claude:git-marketplace']?.url).toBe('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/org/marketplace.git');
- expect(config.marketplaces['claude:git-marketplace']?.branch).toBe('main');
+ expect(config.marketplaces['claude/git-marketplace']?.source).toBe('git');
+ expect(config.marketplaces['claude/git-marketplace']?.url).toBe('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/org/marketplace.git');
+ expect(config.marketplaces['claude/git-marketplace']?.branch).toBe('main');
});
});
diff --git a/tests/commands/plugin-install.test.ts b/tests/commands/plugin-install.test.ts
index 77fa1a2..e6f8565 100644
--- a/tests/commands/plugin-install.test.ts
+++ b/tests/commands/plugin-install.test.ts
@@ -51,7 +51,7 @@ describe('plugin-install', () => {
expect(config.plugins['test-plugin@local']).toBeDefined();
expect(config.plugins['test-plugin@local'].enabled).toBe(true);
- const commandsPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
@@ -200,7 +200,7 @@ describe('plugin-install', () => {
await pluginInstall(options);
- const commandsPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
});
@@ -293,7 +293,7 @@ describe('plugin-install', () => {
const config = JSON.parse(await Bun.file(pluginsPath).text());
expect(config).toEqual(originalConfig);
- const commandsPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin');
expect(await fileExists(commandsPath)).toBe(false);
});
});
@@ -348,7 +348,7 @@ describe('plugin-install', () => {
await pluginInstall(options);
- const commandsPath = join(testDir, '.cursor', 'commands', 'local', 'custom-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'custom-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
});
diff --git a/tests/commands/plugin-uninstall.test.ts b/tests/commands/plugin-uninstall.test.ts
index 484ae00..c76109c 100644
--- a/tests/commands/plugin-uninstall.test.ts
+++ b/tests/commands/plugin-uninstall.test.ts
@@ -130,7 +130,7 @@ describe('plugin-uninstall', () => {
}),
);
- const pluginDir = join(testDir, '.cursor', 'marketplace', 'local', 'my-plugin');
+ const pluginDir = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'my-plugin');
await mkdir(pluginDir, { recursive: true });
await writeFile(join(pluginDir, 'test.txt'), 'test content');
@@ -164,7 +164,7 @@ describe('plugin-uninstall', () => {
}),
);
- const pluginDir = join(testDir, '.cursor', 'marketplace', 'local', 'my-plugin');
+ const pluginDir = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'my-plugin');
await mkdir(pluginDir, { recursive: true });
await writeFile(join(pluginDir, 'test.txt'), 'test content');
@@ -248,7 +248,7 @@ describe('plugin-uninstall', () => {
}),
);
- const pluginDir = join(testDir, '.cursor', 'marketplace', 'local', 'my-plugin');
+ const pluginDir = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'my-plugin');
await mkdir(pluginDir, { recursive: true });
await writeFile(join(pluginDir, 'test.txt'), 'test content');
@@ -289,7 +289,7 @@ describe('plugin-uninstall', () => {
}),
);
- const pluginDir = join(testDir, '.cursor', 'marketplace');
+ const pluginDir = join(testDir, '.cursor', 'commands', 'aipm');
await mkdir(pluginDir, { recursive: true });
const options = {
diff --git a/tests/commands/plugin-update.test.ts b/tests/commands/plugin-update.test.ts
index 891bbb1..5ca2839 100644
--- a/tests/commands/plugin-update.test.ts
+++ b/tests/commands/plugin-update.test.ts
@@ -49,7 +49,7 @@ describe('plugin-update', () => {
await pluginUpdate(options);
- const updatedCommandPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'updated.md');
+ const updatedCommandPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'updated.md');
expect(await fileExists(updatedCommandPath)).toBe(true);
});
@@ -179,8 +179,8 @@ describe('plugin-update', () => {
}),
);
- await mkdir(join(testDir, '.cursor', 'commands', 'local', 'test-plugin'), { recursive: true });
- await writeFile(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'), '# Old version');
+ await mkdir(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin'), { recursive: true });
+ await writeFile(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'), '# Old version');
const options = {
pluginId: 'test-plugin@local',
@@ -190,7 +190,7 @@ describe('plugin-update', () => {
await pluginUpdate(options);
- const newCommandPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'new.md');
+ const newCommandPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'new.md');
expect(await fileExists(newCommandPath)).toBe(false);
});
});
@@ -248,7 +248,7 @@ describe('plugin-update', () => {
await pluginUpdate(options);
- const updatedCommandPath = join(testDir, '.cursor', 'commands', 'local', 'custom-plugin', 'updated.md');
+ const updatedCommandPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'custom-plugin', 'updated.md');
expect(await fileExists(updatedCommandPath)).toBe(true);
});
});
diff --git a/tests/commands/sync-git.test.ts b/tests/commands/sync-git.test.ts
index 314af42..35d6df4 100644
--- a/tests/commands/sync-git.test.ts
+++ b/tests/commands/sync-git.test.ts
@@ -75,7 +75,7 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- const commandsPath = join(testDir, '.cursor', 'commands', 'git', 'git-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'git', 'git-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
@@ -97,9 +97,9 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'git', 'plugin-a', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'git', 'plugin-b', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'git', 'plugin-c', 'test.md'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'git', 'plugin-a', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'git', 'plugin-b', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'git', 'plugin-c', 'test.md'))).toBe(false);
});
test('pulls updates from git marketplace on subsequent syncs', async () => {
@@ -118,7 +118,7 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- const commandsPath = join(testDir, '.cursor', 'commands', 'git', 'update-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'git', 'update-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
await writeFile(join(gitRepoDir, 'update-plugin', 'commands', 'new-file.md'), '# New');
@@ -129,7 +129,7 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- const newFilePath = join(testDir, '.cursor', 'commands', 'git', 'update-plugin', 'new-file.md');
+ const newFilePath = join(testDir, '.cursor', 'commands', 'aipm', 'git', 'update-plugin', 'new-file.md');
expect(await fileExists(newFilePath)).toBe(true);
});
@@ -157,7 +157,7 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- const featurePath = join(testDir, '.cursor', 'commands', 'git', 'branch-plugin', 'feature.md');
+ const featurePath = join(testDir, '.cursor', 'commands', 'aipm', 'git', 'branch-plugin', 'feature.md');
expect(await fileExists(featurePath)).toBe(true);
});
@@ -196,8 +196,10 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'git', 'git-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'local-plugin', 'local-test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'git', 'git-plugin', 'test.md'))).toBe(true);
+ expect(
+ await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'local-plugin', 'local-test.md')),
+ ).toBe(true);
});
test('handles git marketplace without url gracefully', async () => {
@@ -233,6 +235,6 @@ describe('sync command with git sources', () => {
await sync({ cwd: testDir, dryRun: true });
expect(await fileExists(join(cacheDir, 'git'))).toBe(false);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'git', 'dry-plugin'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'git', 'dry-plugin'))).toBe(false);
});
});
diff --git a/tests/commands/sync-marketplace-json.test.ts b/tests/commands/sync-marketplace-json.test.ts
index 886afdc..61f805b 100644
--- a/tests/commands/sync-marketplace-json.test.ts
+++ b/tests/commands/sync-marketplace-json.test.ts
@@ -77,7 +77,7 @@ describe('sync command with marketplace.json', () => {
await sync({ cwd: testDir });
- const commandsPath = join(testDir, '.cursor', 'commands', 'curated', 'my-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'curated', 'my-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
@@ -97,7 +97,7 @@ describe('sync command with marketplace.json', () => {
await sync({ cwd: testDir });
- const commandsPath = join(testDir, '.cursor', 'commands', 'unstructured', 'my-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'unstructured', 'my-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
@@ -136,13 +136,13 @@ describe('sync command with marketplace.json', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'selective', 'included-plugin', 'test.md'))).toBe(
- true,
- );
+ expect(
+ await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'selective', 'included-plugin', 'test.md')),
+ ).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'selective', 'excluded-plugin', 'test.md'))).toBe(
- false,
- );
+ expect(
+ await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'selective', 'excluded-plugin', 'test.md')),
+ ).toBe(false);
});
test('handles nested plugin paths in marketplace.json', async () => {
@@ -174,7 +174,9 @@ describe('sync command with marketplace.json', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'monorepo', 'analyzer', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'monorepo', 'analyzer', 'test.md'))).toBe(
+ true,
+ );
});
test('syncs multiple plugins with different paths', async () => {
@@ -203,7 +205,7 @@ describe('sync command with marketplace.json', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'mixed', 'plugin-a', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'mixed', 'plugin-b', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'mixed', 'plugin-a', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'mixed', 'plugin-b', 'test.md'))).toBe(true);
});
});
diff --git a/tests/commands/sync-rules.test.ts b/tests/commands/sync-rules.test.ts
index e526b48..c9fa96e 100644
--- a/tests/commands/sync-rules.test.ts
+++ b/tests/commands/sync-rules.test.ts
@@ -59,7 +59,7 @@ Rule content here.`,
await sync({ cwd: testDir });
- const outputPath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'my-rule.mdc');
+ const outputPath = join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'my-rule.mdc');
expect(await fileExists(outputPath)).toBe(true);
@@ -103,7 +103,7 @@ setting: value
await sync({ cwd: testDir });
- const outputPath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'my-rule.mdc');
+ const outputPath = join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'my-rule.mdc');
expect(await fileExists(outputPath)).toBe(true);
@@ -159,7 +159,7 @@ priority: high`,
await sync({ cwd: testDir });
- const outputPath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'my-rule.mdc');
+ const outputPath = join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'my-rule.mdc');
expect(await fileExists(outputPath)).toBe(true);
@@ -227,7 +227,7 @@ alwaysApply: false
await sync({ cwd: testDir });
// Check rule with override
- const overridePath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'rule-with-override.mdc');
+ const overridePath = join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'rule-with-override.mdc');
expect(await fileExists(overridePath)).toBe(true);
const overrideContent = await readFile(overridePath, 'utf-8');
expect(overrideContent).toContain('cursorMode: strict');
@@ -235,7 +235,15 @@ alwaysApply: false
expect(overrideContent).toContain('# Rule With Override');
// Check rule without override
- const noOverridePath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'rule-without-override.mdc');
+ const noOverridePath = join(
+ testDir,
+ '.cursor',
+ 'rules',
+ 'aipm',
+ 'local',
+ 'test-plugin',
+ 'rule-without-override.mdc',
+ );
expect(await fileExists(noOverridePath)).toBe(true);
const noOverrideContent = await readFile(noOverridePath, 'utf-8');
expect(noOverrideContent).toContain('alwaysApply: false');
@@ -270,7 +278,7 @@ alwaysApply: false
await sync({ cwd: testDir });
- const cursorYamlPath = join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'my-rule.cursor.yaml');
+ const cursorYamlPath = join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'my-rule.cursor.yaml');
expect(await fileExists(cursorYamlPath)).toBe(false);
});
diff --git a/tests/commands/sync.test.ts b/tests/commands/sync.test.ts
index 2ca2d22..9b9e160 100644
--- a/tests/commands/sync.test.ts
+++ b/tests/commands/sync.test.ts
@@ -54,7 +54,7 @@ describe('sync command', () => {
await sync({ cwd: testDir });
- const commandsPath = join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md');
+ const commandsPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md');
expect(await fileExists(commandsPath)).toBe(true);
});
@@ -77,8 +77,8 @@ describe('sync command', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'plugin1', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'plugin2', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'plugin1', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'plugin2', 'test.md'))).toBe(true);
});
test('only syncs enabled plugins', async () => {
@@ -105,8 +105,10 @@ describe('sync command', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'enabled', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'disabled', 'test.md'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'enabled', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'disabled', 'test.md'))).toBe(
+ false,
+ );
});
test('clears marketplace directory before syncing', async () => {
@@ -123,7 +125,7 @@ describe('sync command', () => {
await sync({ cwd: testDir });
- const oldPluginPath = join(testDir, '.cursor', 'commands', 'local', 'plugin1', 'test.md');
+ const oldPluginPath = join(testDir, '.cursor', 'commands', 'aipm', 'local', 'plugin1', 'test.md');
expect(await fileExists(oldPluginPath)).toBe(true);
await createMockPlugin('plugin2');
@@ -139,8 +141,8 @@ describe('sync command', () => {
await sync({ cwd: testDir });
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'plugin1', 'test.md'))).toBe(false);
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'plugin2', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'plugin1', 'test.md'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'plugin2', 'test.md'))).toBe(true);
});
});
@@ -345,8 +347,12 @@ describe('sync command', () => {
await sync({ cwd: testDir });
// Nothing should be synced
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'))).toBe(false);
- expect(await fileExists(join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'test.mdc'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ false,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'test.mdc'))).toBe(
+ false,
+ );
});
test('syncs all types when include is "all"', async () => {
@@ -370,11 +376,19 @@ describe('sync command', () => {
await sync({ cwd: testDir });
// All types should be synced
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'test.mdc'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'agents', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'skills', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'hooks', 'local', 'test-plugin', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'test.mdc'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'agents', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'skills', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'hooks', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(true);
});
test('syncs only enabled types when include is object', async () => {
@@ -409,11 +423,15 @@ describe('sync command', () => {
await sync({ cwd: testDir });
// Only rules and commands should be synced
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'test.mdc'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'agents', 'local', 'test-plugin'))).toBe(false);
- expect(await fileExists(join(testDir, '.cursor', 'skills', 'local', 'test-plugin'))).toBe(false);
- expect(await fileExists(join(testDir, '.cursor', 'hooks', 'local', 'test-plugin'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'test.mdc'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'agents', 'aipm', 'local', 'test-plugin'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'skills', 'aipm', 'local', 'test-plugin'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'hooks', 'aipm', 'local', 'test-plugin'))).toBe(false);
});
test('defaults to true for unspecified types in include object', async () => {
@@ -444,11 +462,17 @@ describe('sync command', () => {
await sync({ cwd: testDir });
// Everything except agents should be synced (default to true)
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'test.mdc'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'skills', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'hooks', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'agents', 'local', 'test-plugin'))).toBe(false);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'test.mdc'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'skills', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'hooks', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'agents', 'aipm', 'local', 'test-plugin'))).toBe(false);
});
test('syncs all types by default when no integrations config', async () => {
@@ -467,11 +491,19 @@ describe('sync command', () => {
await sync({ cwd: testDir });
// All types should be synced
- expect(await fileExists(join(testDir, '.cursor', 'commands', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'rules', 'local', 'test-plugin', 'test.mdc'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'agents', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'skills', 'local', 'test-plugin', 'test.md'))).toBe(true);
- expect(await fileExists(join(testDir, '.cursor', 'hooks', 'local', 'test-plugin', 'test.md'))).toBe(true);
+ expect(await fileExists(join(testDir, '.cursor', 'commands', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'rules', 'aipm', 'local', 'test-plugin', 'test.mdc'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'agents', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'skills', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(
+ true,
+ );
+ expect(await fileExists(join(testDir, '.cursor', 'hooks', 'aipm', 'local', 'test-plugin', 'test.md'))).toBe(true);
});
});
});