Skip to content
Open
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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"WebFetch(domain:github.com)",
"Bash(npx tsc:*)",
"Bash(npx vitest:*)",
"Bash(git clone:*)"
"Bash(git clone:*)",
"mcp__ide__getDiagnostics"
]
}
}
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ acplugin scan anthropics/claude-code

## Supported Conversions

| Resource | Codex CLI | OpenCode | Cursor | Antigravity |
| ---------------- | ------------------------- | ------------------------- | --------------------- | ----------------------- |
| **Skills** | `.agents/skills/` | `.opencode/skills/` | `.cursor/skills/` | `.agent/skills/` |
| **Instructions** | `AGENTS.md` | `AGENTS.md` | `.cursor/rules/*.mdc` | `GEMINI.md` |
| **MCP Servers** | `.codex/config.toml` | `opencode.json` | `.cursor/mcp.json` | `.gemini/settings.json` |
| **Agents** | `.codex/agents/*.toml` | `.opencode/agents/*.md` | `.cursor/agents/*.md` | `.gemini/agents/*.md` |
| **Commands** | Converted to Skills | `.opencode/commands/` | `.cursor/commands/` | Converted to Skills |
| **Hooks** | Documented in `AGENTS.md` | Documented in `AGENTS.md` | Warnings only | Warnings only |
| Resource | Codex CLI | OpenCode | Cursor | Antigravity |
| -------------------- | --------------------------- | ------------------------- | ------------------------------ | ----------------------- |
| **Skills** | `.agents/skills/` | `.opencode/skills/` | `.cursor/skills/` | `.agent/skills/` |
| **Instructions** | `AGENTS.md` | `AGENTS.md` | `.cursor/rules/*.mdc` | `GEMINI.md` |
| **MCP Servers** | `.codex/config.toml` | `opencode.json` | `.cursor/mcp.json` | `.gemini/settings.json` |
| **Agents** | `.codex/agents/*.toml` | `.opencode/agents/*.md` | `.cursor/agents/*.md` | `.gemini/agents/*.md` |
| **Commands** | Converted to Skills | `.opencode/commands/` | `.cursor/commands/` | Converted to Skills |
| **Hooks** | Documented in `AGENTS.md` | Documented in `AGENTS.md` | `.cursor/hooks.json` | Warnings only |
| **Plugin Manifest** | `.codex-plugin/plugin.json` | — | `.cursor-plugin/plugin.json` | — |
| **Marketplace** | `.agents/plugins/marketplace.json` | — | `.cursor-plugin/marketplace.json` | — |
| **Scripts** | `scripts/` (preserved) | `scripts/` (preserved) | `.cursor/scripts/` (preserved) | `scripts/` (preserved) |

### Model Mapping

Expand Down
41 changes: 22 additions & 19 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,25 @@ acplugin scan anthropics/claude-code

## 支持的转换

| 资源类型 | Codex CLI | OpenCode | Cursor | Antigravity |
|---------|-----------|----------|--------|-------------|
| **Skills** | `.agents/skills/` | `.opencode/skills/` | `.cursor/skills/` | `.agent/skills/` |
| **指令** | `AGENTS.md` | `AGENTS.md` | `.cursor/rules/*.mdc` | `GEMINI.md` |
| **MCP 服务器** | `.codex/config.toml` | `opencode.json` | `.cursor/mcp.json` | `.gemini/settings.json` |
| **Agents** | `.codex/agents/*.toml` | `.opencode/agents/*.md` | `.cursor/agents/*.md` | `.gemini/agents/*.md` |
| **Commands** | 转换为 Skills | `.opencode/commands/` | `.cursor/commands/` | 转换为 Skills |
| **Hooks** | 记录在 `AGENTS.md` | 记录在 `AGENTS.md` | 仅输出警告 | 仅输出警告 |
| 资源类型 | Codex CLI | OpenCode | Cursor | Antigravity |
| -------------------- | ---------------------------------- | ----------------------- | --------------------------------- | ----------------------- |
| **Skills** | `.agents/skills/` | `.opencode/skills/` | `.cursor/skills/` | `.agent/skills/` |
| **指令** | `AGENTS.md` | `AGENTS.md` | `.cursor/rules/*.mdc` | `GEMINI.md` |
| **MCP 服务器** | `.codex/config.toml` | `opencode.json` | `.cursor/mcp.json` | `.gemini/settings.json` |
| **Agents** | `.codex/agents/*.toml` | `.opencode/agents/*.md` | `.cursor/agents/*.md` | `.gemini/agents/*.md` |
| **Commands** | 转换为 Skills | `.opencode/commands/` | `.cursor/commands/` | 转换为 Skills |
| **Hooks** | 记录在 `AGENTS.md` | 记录在 `AGENTS.md` | `.cursor/hooks.json` | 仅输出警告 |
| **plugin.json** | `.codex-plugin/plugin.json` | — | `.cursor-plugin/plugin.json` | — |
| **marketplace.json** | `.agents/plugins/marketplace.json` | — | `.cursor-plugin/marketplace.json` | — |
| **Scripts** | `scripts/`(保持原路径) | `scripts/`(保持原路径) | `.cursor/scripts/`(保持原路径) | `scripts/`(保持原路径)|

### 模型映射

| Claude Code | → Codex | → Antigravity |
|-------------|---------|---------------|
| `sonnet` / `opus` | `gpt-5.4` | `gemini-3-pro` |
| `haiku` | `gpt-5.4` | `gemini-3-flash` |
| (未指定) | `gpt-5.4` | `gemini-3-pro` |
| Claude Code | → Codex | → Antigravity |
| ----------------- | --------- | ---------------- |
| `sonnet` / `opus` | `gpt-5.4` | `gemini-3-pro` |
| `haiku` | `gpt-5.4` | `gemini-3-flash` |
| (未指定) | `gpt-5.4` | `gemini-3-pro` |

OpenCode 和 Cursor 保持原始模型值不映射。

Expand Down Expand Up @@ -92,13 +95,13 @@ acplugin convert . --dry-run # 预览模式,不写入文件

**选项:**

| 选项 | 说明 |
|------|------|
| 选项 | 说明 |
| ---------------------- | ------------------------------------------------------------------ |
| `-t, --to <platforms>` | 目标平台(逗号分隔:`codex`、`opencode`、`cursor`、`antigravity`) |
| `-o, --output <path>` | 输出目录 |
| `-a, --all` | 全部转换,跳过交互选择 |
| `-p, --path <subpath>` | 仓库内子路径 |
| `--dry-run` | 预览生成的文件,不实际写入 |
| `-o, --output <path>` | 输出目录 |
| `-a, --all` | 全部转换,跳过交互选择 |
| `-p, --path <subpath>` | 仓库内子路径 |
| `--dry-run` | 预览生成的文件,不实际写入 |

## 使用示例

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('convertAgent', () => {

it('converts to cursor as agent file', () => {
const result = convertAgent(sampleAgent, 'cursor');
expect(result.path).toBe('.cursor/agents/code-reviewer.md');
expect(result.path).toBe('agents/code-reviewer.md');
expect(result.content).toContain('name: code-reviewer');
expect(result.content).toContain('description: Reviews code for bugs');
});
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('convertCommand', () => {

it('converts to cursor as command file', () => {
const result = convertCommand(sampleCommand, 'cursor');
expect(result.path).toBe('.cursor/commands/deploy.md');
expect(result.path).toBe('commands/deploy.md');
expect(result.content).toContain('Deploy to $1');
});
});
105 changes: 105 additions & 0 deletions src/__tests__/cursor-writer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, it, expect } from 'vitest';
import { generateCursor } from '../writer/cursor.js';
import type { ScanResult } from '../types.js';

function makeScan(overrides: Partial<ScanResult> = {}): ScanResult {
return {
skills: [],
instructions: [],
agents: [],
commands: [],
mcp: null,
hooks: null,
pluginFiles: [],
...overrides,
} as ScanResult;
}

describe('generateCursor — addCursorPrefix', () => {
it('adds .cursor/ prefix to skill paths', () => {
const result = generateCursor(makeScan({
skills: [{
dirName: 'my-skill',
frontmatter: { name: 'my-skill' },
body: 'Do stuff',
sourcePath: '/fake/skills/my-skill/SKILL.md',
auxFiles: [],
}],
}));
const skill = result.files.find(f => f.type === 'skill');
expect(skill?.path).toBe('.cursor/skills/my-skill/SKILL.md');
});

it('adds .cursor/ prefix to agent paths', () => {
const result = generateCursor(makeScan({
agents: [{
fileName: 'helper',
frontmatter: { name: 'helper', description: 'A helper' },
body: 'Help the user',
sourcePath: '/fake/agents/helper.md',
}],
}));
const agent = result.files.find(f => f.type === 'agent');
expect(agent?.path).toBe('.cursor/agents/helper.md');
});

it('adds .cursor/ prefix to command paths', () => {
const result = generateCursor(makeScan({
commands: [{ name: 'deploy', content: 'Deploy it', sourcePath: '/fake/commands/deploy.md' }],
}));
const cmd = result.files.find(f => f.type === 'command');
expect(cmd?.path).toBe('.cursor/commands/deploy.md');
});

it('adds .cursor/ prefix to instruction paths', () => {
const result = generateCursor(makeScan({
instructions: [{
fileName: 'testing.md',
content: 'Always test',
isRule: true,
sourcePath: '/fake/rules/testing.md',
}],
}));
const rule = result.files.find(f => f.type === 'instruction');
expect(rule?.path).toBe('.cursor/rules/testing.mdc');
});

it('adds .cursor/ prefix to mcp.json', () => {
const result = generateCursor(makeScan({
mcp: {
servers: [{ name: 'fs', command: 'npx', args: ['-y', 'fs-server'] }],
sourcePath: '/fake/.mcp.json',
},
}));
const mcp = result.files.find(f => f.type === 'mcp');
expect(mcp?.path).toBe('.cursor/mcp.json');
});

it('adds .cursor/ prefix to hooks.json', () => {
const result = generateCursor(makeScan({
hooks: {
SessionStart: [{
matcher: '',
hooks: [{ type: 'command', command: 'echo hello' }],
}],
},
}));
const hook = result.files.find(f => f.type === 'hook');
expect(hook?.path).toBe('.cursor/hooks.json');
});

it('adds .cursor/ prefix to pluginFiles (scripts/)', () => {
const result = generateCursor(makeScan({
pluginFiles: [{ relativePath: 'scripts/setup.sh', content: '#!/bin/bash' }],
}));
const script = result.files.find(f => f.type === 'resource');
expect(script?.path).toBe('.cursor/scripts/setup.sh');
});

it('does NOT add prefix to .cursor-plugin/ manifest paths', () => {
const result = generateCursor(makeScan());
const manifest = result.files.find(f => f.path === '.cursor-plugin/plugin.json');
expect(manifest).toBeDefined();
expect(manifest?.path).toBe('.cursor-plugin/plugin.json');
});
});
4 changes: 2 additions & 2 deletions src/__tests__/instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ describe('mergeInstructions', () => {
it('creates separate .mdc files for cursor', () => {
const result = mergeInstructions([claudeMd, rule], 'cursor');
expect(result).toHaveLength(2);
expect(result[0].path).toBe('.cursor/rules/claude-instructions.mdc');
expect(result[0].path).toBe('rules/claude-instructions.mdc');
expect(result[0].content).toContain('alwaysApply: true');
expect(result[1].path).toBe('.cursor/rules/testing.mdc');
expect(result[1].path).toBe('rules/testing.mdc');
expect(result[1].content).toContain('alwaysApply: true');
});

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('convertMCP', () => {

it('converts to cursor JSON format', () => {
const result = convertMCP(sampleMCP, 'cursor');
expect(result.path).toBe('.cursor/mcp.json');
expect(result.path).toBe('mcp.json');
const data = JSON.parse(result.content);
expect(data.mcpServers.filesystem.command).toBe('npx');
expect(data.mcpServers.github.url).toBe('https://api.github.com/mcp');
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/pluginManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe('convertPluginManifestForCursor', () => {
expect(manifest.name).toBe('my-plugin');
expect(manifest.displayName).toBe('My Plugin');
expect(manifest.logo).toBe('./assets/logo.png');
expect(manifest.skills).toBe('./skills/');
expect(manifest.skills).toBe('./.cursor/skills/');
});

it('falls back to interface.displayName when no top-level displayName', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('convertSkill', () => {

it('converts to cursor path', () => {
const result = convertSkill(sampleSkill, 'cursor');
expect(result.path).toBe('.cursor/skills/test-skill/SKILL.md');
expect(result.path).toBe('skills/test-skill/SKILL.md');
});

it('preserves name and description in frontmatter', () => {
Expand Down
27 changes: 13 additions & 14 deletions src/__tests__/superpowers-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,38 +338,37 @@ describe.skipIf(!repoExists)('superpowers plugin integration', () => {
const manifest = JSON.parse(pluginJson!.content);
expect(manifest.name).toBe('superpowers');
expect(manifest.version).toBe('5.0.5');
expect(manifest.skills).toBe('./skills/');
expect(manifest.agents).toBe('./agents/');
expect(manifest.commands).toBe('./commands/');
expect(manifest.skills).toBe('./.cursor/skills/');
expect(manifest.agents).toBe('./.cursor/agents/');
expect(manifest.commands).toBe('./.cursor/commands/');
// New fields from plugin.json passthrough
expect(manifest.hooks).toBe('./hooks/hooks-cursor.json');
expect(manifest.hooks).toBe('./.cursor/hooks.json');
expect(manifest.author).toEqual({ name: 'Jesse Vincent', email: 'jesse@fsck.com' });
});

it('remaps skill paths from .cursor/ to plugin root', () => {
it('adds .cursor/ prefix to skill paths', () => {
const skillFiles = result.files.filter(f => f.type === 'skill');
for (const f of skillFiles) {
expect(f.path).toMatch(/^skills\//);
expect(f.path).not.toMatch(/^\.cursor\//);
expect(f.path).toMatch(/^\.cursor\/skills\//);
}
});

it('remaps agent paths', () => {
it('adds .cursor/ prefix to agent paths', () => {
const agentFiles = result.files.filter(f => f.type === 'agent');
expect(agentFiles).toHaveLength(1);
expect(agentFiles[0].path).toBe('agents/code-reviewer.md');
expect(agentFiles[0].path).toBe('.cursor/agents/code-reviewer.md');
});

it('remaps command paths', () => {
it('adds .cursor/ prefix to command paths', () => {
const cmdFiles = result.files.filter(f => f.type === 'command');
expect(cmdFiles).toHaveLength(3);
for (const f of cmdFiles) {
expect(f.path).toMatch(/^commands\//);
expect(f.path).toMatch(/^\.cursor\/commands\//);
}
});

it('generates hooks/hooks-cursor.json with camelCase events', () => {
const hooksFile = result.files.find(f => f.path === 'hooks/hooks-cursor.json');
it('generates .cursor/hooks.json with camelCase events', () => {
const hooksFile = result.files.find(f => f.path === '.cursor/hooks.json');
expect(hooksFile).toBeDefined();
const hooksData = JSON.parse(hooksFile!.content);
expect(hooksData.version).toBe(1);
Expand All @@ -379,7 +378,7 @@ describe.skipIf(!repoExists)('superpowers plugin integration', () => {
});

it('cursor hooks use relative paths (no ${CLAUDE_PLUGIN_ROOT})', () => {
const hooksFile = result.files.find(f => f.path === 'hooks/hooks-cursor.json');
const hooksFile = result.files.find(f => f.path === '.cursor/hooks.json');
expect(hooksFile).toBeDefined();
expect(hooksFile!.content).not.toContain('CLAUDE_PLUGIN_ROOT');
expect(hooksFile!.content).toContain('./hooks/');
Expand Down
2 changes: 1 addition & 1 deletion src/converter/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function convertToCursor(agent: Agent): ConvertedFile {
}

const content = stringifyFrontmatter(fm, agent.body);
return { path: `.cursor/agents/${agent.fileName}.md`, content, type: 'agent' };
return { path: `agents/${agent.fileName}.md`, content, type: 'agent' };
}

// --- Antigravity: .gemini/agents/*.md (YAML frontmatter) ---
Expand Down
2 changes: 1 addition & 1 deletion src/converter/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function convertToOpenCode(command: Command): ConvertedFile {

function convertToCursor(command: Command): ConvertedFile {
return {
path: `.cursor/commands/${command.name}.md`,
path: `commands/${command.name}.md`,
content: command.content,
type: 'command',
};
Expand Down
2 changes: 1 addition & 1 deletion src/converter/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function convertCursorHooks(hooks: Hooks): HookReport {
if (Object.keys(cursorHooks).length > 0) {
const content = JSON.stringify({ version: 1, hooks: cursorHooks }, null, 2);
converted.push({
path: 'hooks/hooks-cursor.json',
path: 'hooks.json',
content,
type: 'hook',
});
Expand Down
4 changes: 2 additions & 2 deletions src/converter/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function convertToCursor(instruction: Instruction): ConvertedFile {
alwaysApply: true,
};
return {
path: `.cursor/rules/${name}.mdc`,
path: `rules/${name}.mdc`,
content: stringifyFrontmatter(frontmatter, instruction.content),
type: 'instruction',
};
Expand All @@ -54,7 +54,7 @@ function convertToCursor(instruction: Instruction): ConvertedFile {
alwaysApply: true,
};
return {
path: '.cursor/rules/claude-instructions.mdc',
path: 'rules/claude-instructions.mdc',
content: stringifyFrontmatter(frontmatter, instruction.content),
type: 'instruction',
};
Expand Down
2 changes: 1 addition & 1 deletion src/converter/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function convertToCursor(mcp: MCPConfig): ConvertedFile {

const content = JSON.stringify({ mcpServers }, null, 2);
return {
path: '.cursor/mcp.json',
path: 'mcp.json',
content,
type: 'mcp',
};
Expand Down
12 changes: 6 additions & 6 deletions src/converter/pluginManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ const PLATFORM_PATHS: Record<ManifestPlatform, PlatformPaths> = {
cursor: {
pluginJson: '.cursor-plugin/plugin.json',
marketplaceJson: '.cursor-plugin/marketplace.json',
skills: './skills/',
agents: './agents/',
commands: './commands/',
instructions: './rules/',
mcp: './mcp.json',
hooks: './hooks/hooks-cursor.json',
skills: './.cursor/skills/',
agents: './.cursor/agents/',
commands: './.cursor/commands/',
instructions: './.cursor/rules/',
mcp: './.cursor/mcp.json',
hooks: './.cursor/hooks.json',
},
};

Expand Down
2 changes: 1 addition & 1 deletion src/converter/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function getSkillOutputPath(platform: Platform, dirName: string): string {
case 'opencode':
return `.opencode/skills/${dirName}/SKILL.md`;
case 'cursor':
return `.cursor/skills/${dirName}/SKILL.md`;
return `skills/${dirName}/SKILL.md`;
case 'antigravity':
return `.agent/skills/${dirName}/SKILL.md`;
}
Expand Down
Loading