@every-env/compound-plugin 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/marketplace.json +25 -0
- package/CHANGELOG.md +21 -0
- package/README.md +18 -8
- package/bun.lock +1 -0
- package/docs/brainstorms/2026-02-14-copilot-converter-target-brainstorm.md +117 -0
- package/docs/brainstorms/2026-02-17-copilot-skill-naming-brainstorm.md +30 -0
- package/docs/plans/2026-02-14-feat-add-copilot-converter-target-plan.md +328 -0
- package/docs/specs/copilot.md +122 -0
- package/docs/specs/kiro.md +171 -0
- package/package.json +1 -1
- package/plugins/coding-tutor/.cursor-plugin/plugin.json +21 -0
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/.cursor-plugin/plugin.json +31 -0
- package/plugins/compound-engineering/.mcp.json +8 -0
- package/plugins/compound-engineering/CHANGELOG.md +10 -0
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +2 -2
- package/plugins/compound-engineering/commands/workflows/plan.md +15 -1
- package/src/commands/convert.ts +2 -1
- package/src/commands/install.ts +9 -1
- package/src/commands/sync.ts +8 -8
- package/src/converters/{claude-to-cursor.ts → claude-to-copilot.ts} +93 -49
- package/src/converters/claude-to-kiro.ts +262 -0
- package/src/sync/{cursor.ts → copilot.ts} +36 -14
- package/src/targets/copilot.ts +48 -0
- package/src/targets/index.ts +18 -9
- package/src/targets/kiro.ts +122 -0
- package/src/types/copilot.ts +31 -0
- package/src/types/kiro.ts +44 -0
- package/src/utils/frontmatter.ts +1 -1
- package/tests/copilot-converter.test.ts +467 -0
- package/tests/copilot-writer.test.ts +189 -0
- package/tests/kiro-converter.test.ts +381 -0
- package/tests/kiro-writer.test.ts +273 -0
- package/tests/sync-copilot.test.ts +148 -0
- package/src/targets/cursor.ts +0 -48
- package/src/types/cursor.ts +0 -29
- package/tests/cursor-converter.test.ts +0 -347
- package/tests/cursor-writer.test.ts +0 -137
- package/tests/sync-cursor.test.ts +0 -92
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { promises as fs } from "fs"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import os from "os"
|
|
5
|
-
import { writeCursorBundle } from "../src/targets/cursor"
|
|
6
|
-
import type { CursorBundle } from "../src/types/cursor"
|
|
7
|
-
|
|
8
|
-
async function exists(filePath: string): Promise<boolean> {
|
|
9
|
-
try {
|
|
10
|
-
await fs.access(filePath)
|
|
11
|
-
return true
|
|
12
|
-
} catch {
|
|
13
|
-
return false
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe("writeCursorBundle", () => {
|
|
18
|
-
test("writes rules, commands, skills, and mcp.json", async () => {
|
|
19
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-test-"))
|
|
20
|
-
const bundle: CursorBundle = {
|
|
21
|
-
rules: [{ name: "security-reviewer", content: "---\ndescription: Security\nglobs: \"\"\nalwaysApply: false\n---\n\nReview code." }],
|
|
22
|
-
commands: [{ name: "plan", content: "<!-- Planning -->\n\nPlan the work." }],
|
|
23
|
-
skillDirs: [
|
|
24
|
-
{
|
|
25
|
-
name: "skill-one",
|
|
26
|
-
sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"),
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
mcpServers: {
|
|
30
|
-
playwright: { command: "npx", args: ["-y", "@anthropic/mcp-playwright"] },
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
await writeCursorBundle(tempRoot, bundle)
|
|
35
|
-
|
|
36
|
-
expect(await exists(path.join(tempRoot, ".cursor", "rules", "security-reviewer.mdc"))).toBe(true)
|
|
37
|
-
expect(await exists(path.join(tempRoot, ".cursor", "commands", "plan.md"))).toBe(true)
|
|
38
|
-
expect(await exists(path.join(tempRoot, ".cursor", "skills", "skill-one", "SKILL.md"))).toBe(true)
|
|
39
|
-
expect(await exists(path.join(tempRoot, ".cursor", "mcp.json"))).toBe(true)
|
|
40
|
-
|
|
41
|
-
const ruleContent = await fs.readFile(
|
|
42
|
-
path.join(tempRoot, ".cursor", "rules", "security-reviewer.mdc"),
|
|
43
|
-
"utf8",
|
|
44
|
-
)
|
|
45
|
-
expect(ruleContent).toContain("Review code.")
|
|
46
|
-
|
|
47
|
-
const commandContent = await fs.readFile(
|
|
48
|
-
path.join(tempRoot, ".cursor", "commands", "plan.md"),
|
|
49
|
-
"utf8",
|
|
50
|
-
)
|
|
51
|
-
expect(commandContent).toContain("Plan the work.")
|
|
52
|
-
|
|
53
|
-
const mcpContent = JSON.parse(
|
|
54
|
-
await fs.readFile(path.join(tempRoot, ".cursor", "mcp.json"), "utf8"),
|
|
55
|
-
)
|
|
56
|
-
expect(mcpContent.mcpServers.playwright.command).toBe("npx")
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test("writes directly into a .cursor output root without double-nesting", async () => {
|
|
60
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-home-"))
|
|
61
|
-
const cursorRoot = path.join(tempRoot, ".cursor")
|
|
62
|
-
const bundle: CursorBundle = {
|
|
63
|
-
rules: [{ name: "reviewer", content: "Reviewer rule content" }],
|
|
64
|
-
commands: [{ name: "plan", content: "Plan content" }],
|
|
65
|
-
skillDirs: [],
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await writeCursorBundle(cursorRoot, bundle)
|
|
69
|
-
|
|
70
|
-
expect(await exists(path.join(cursorRoot, "rules", "reviewer.mdc"))).toBe(true)
|
|
71
|
-
expect(await exists(path.join(cursorRoot, "commands", "plan.md"))).toBe(true)
|
|
72
|
-
// Should NOT double-nest under .cursor/.cursor
|
|
73
|
-
expect(await exists(path.join(cursorRoot, ".cursor"))).toBe(false)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test("handles empty bundles gracefully", async () => {
|
|
77
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-empty-"))
|
|
78
|
-
const bundle: CursorBundle = {
|
|
79
|
-
rules: [],
|
|
80
|
-
commands: [],
|
|
81
|
-
skillDirs: [],
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await writeCursorBundle(tempRoot, bundle)
|
|
85
|
-
expect(await exists(tempRoot)).toBe(true)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test("writes multiple rules as separate .mdc files", async () => {
|
|
89
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-multi-"))
|
|
90
|
-
const cursorRoot = path.join(tempRoot, ".cursor")
|
|
91
|
-
const bundle: CursorBundle = {
|
|
92
|
-
rules: [
|
|
93
|
-
{ name: "security-sentinel", content: "Security rules" },
|
|
94
|
-
{ name: "performance-oracle", content: "Performance rules" },
|
|
95
|
-
{ name: "code-simplicity-reviewer", content: "Simplicity rules" },
|
|
96
|
-
],
|
|
97
|
-
commands: [],
|
|
98
|
-
skillDirs: [],
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await writeCursorBundle(cursorRoot, bundle)
|
|
102
|
-
|
|
103
|
-
expect(await exists(path.join(cursorRoot, "rules", "security-sentinel.mdc"))).toBe(true)
|
|
104
|
-
expect(await exists(path.join(cursorRoot, "rules", "performance-oracle.mdc"))).toBe(true)
|
|
105
|
-
expect(await exists(path.join(cursorRoot, "rules", "code-simplicity-reviewer.mdc"))).toBe(true)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("backs up existing mcp.json before overwriting", async () => {
|
|
109
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-backup-"))
|
|
110
|
-
const cursorRoot = path.join(tempRoot, ".cursor")
|
|
111
|
-
await fs.mkdir(cursorRoot, { recursive: true })
|
|
112
|
-
|
|
113
|
-
// Write an existing mcp.json
|
|
114
|
-
const mcpPath = path.join(cursorRoot, "mcp.json")
|
|
115
|
-
await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: { old: { command: "old-cmd" } } }))
|
|
116
|
-
|
|
117
|
-
const bundle: CursorBundle = {
|
|
118
|
-
rules: [],
|
|
119
|
-
commands: [],
|
|
120
|
-
skillDirs: [],
|
|
121
|
-
mcpServers: {
|
|
122
|
-
newServer: { command: "new-cmd" },
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
await writeCursorBundle(cursorRoot, bundle)
|
|
127
|
-
|
|
128
|
-
// New mcp.json should have the new content
|
|
129
|
-
const newContent = JSON.parse(await fs.readFile(mcpPath, "utf8"))
|
|
130
|
-
expect(newContent.mcpServers.newServer.command).toBe("new-cmd")
|
|
131
|
-
|
|
132
|
-
// A backup file should exist
|
|
133
|
-
const files = await fs.readdir(cursorRoot)
|
|
134
|
-
const backupFiles = files.filter((f) => f.startsWith("mcp.json.bak."))
|
|
135
|
-
expect(backupFiles.length).toBeGreaterThanOrEqual(1)
|
|
136
|
-
})
|
|
137
|
-
})
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { promises as fs } from "fs"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import os from "os"
|
|
5
|
-
import { syncToCursor } from "../src/sync/cursor"
|
|
6
|
-
import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
|
|
7
|
-
|
|
8
|
-
describe("syncToCursor", () => {
|
|
9
|
-
test("symlinks skills and writes mcp.json", async () => {
|
|
10
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-cursor-"))
|
|
11
|
-
const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
|
|
12
|
-
|
|
13
|
-
const config: ClaudeHomeConfig = {
|
|
14
|
-
skills: [
|
|
15
|
-
{
|
|
16
|
-
name: "skill-one",
|
|
17
|
-
sourceDir: fixtureSkillDir,
|
|
18
|
-
skillPath: path.join(fixtureSkillDir, "SKILL.md"),
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
mcpServers: {
|
|
22
|
-
context7: { url: "https://mcp.context7.com/mcp" },
|
|
23
|
-
local: { command: "echo", args: ["hello"], env: { FOO: "bar" } },
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
await syncToCursor(config, tempRoot)
|
|
28
|
-
|
|
29
|
-
// Check skill symlink
|
|
30
|
-
const linkedSkillPath = path.join(tempRoot, "skills", "skill-one")
|
|
31
|
-
const linkedStat = await fs.lstat(linkedSkillPath)
|
|
32
|
-
expect(linkedStat.isSymbolicLink()).toBe(true)
|
|
33
|
-
|
|
34
|
-
// Check mcp.json
|
|
35
|
-
const mcpPath = path.join(tempRoot, "mcp.json")
|
|
36
|
-
const mcpConfig = JSON.parse(await fs.readFile(mcpPath, "utf8")) as {
|
|
37
|
-
mcpServers: Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string> }>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
expect(mcpConfig.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
|
|
41
|
-
expect(mcpConfig.mcpServers.local?.command).toBe("echo")
|
|
42
|
-
expect(mcpConfig.mcpServers.local?.args).toEqual(["hello"])
|
|
43
|
-
expect(mcpConfig.mcpServers.local?.env).toEqual({ FOO: "bar" })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("merges existing mcp.json", async () => {
|
|
47
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-cursor-merge-"))
|
|
48
|
-
const mcpPath = path.join(tempRoot, "mcp.json")
|
|
49
|
-
|
|
50
|
-
await fs.writeFile(
|
|
51
|
-
mcpPath,
|
|
52
|
-
JSON.stringify({ mcpServers: { existing: { command: "node", args: ["server.js"] } } }, null, 2),
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
const config: ClaudeHomeConfig = {
|
|
56
|
-
skills: [],
|
|
57
|
-
mcpServers: {
|
|
58
|
-
context7: { url: "https://mcp.context7.com/mcp" },
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
await syncToCursor(config, tempRoot)
|
|
63
|
-
|
|
64
|
-
const merged = JSON.parse(await fs.readFile(mcpPath, "utf8")) as {
|
|
65
|
-
mcpServers: Record<string, { command?: string; url?: string }>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
expect(merged.mcpServers.existing?.command).toBe("node")
|
|
69
|
-
expect(merged.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test("does not write mcp.json when no MCP servers", async () => {
|
|
73
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-cursor-nomcp-"))
|
|
74
|
-
const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
|
|
75
|
-
|
|
76
|
-
const config: ClaudeHomeConfig = {
|
|
77
|
-
skills: [
|
|
78
|
-
{
|
|
79
|
-
name: "skill-one",
|
|
80
|
-
sourceDir: fixtureSkillDir,
|
|
81
|
-
skillPath: path.join(fixtureSkillDir, "SKILL.md"),
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
mcpServers: {},
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
await syncToCursor(config, tempRoot)
|
|
88
|
-
|
|
89
|
-
const mcpExists = await fs.access(path.join(tempRoot, "mcp.json")).then(() => true).catch(() => false)
|
|
90
|
-
expect(mcpExists).toBe(false)
|
|
91
|
-
})
|
|
92
|
-
})
|