@every-env/compound-plugin 0.7.0 → 0.8.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.
Files changed (33) hide show
  1. package/.cursor-plugin/marketplace.json +25 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +14 -8
  4. package/bun.lock +1 -0
  5. package/docs/brainstorms/2026-02-14-copilot-converter-target-brainstorm.md +117 -0
  6. package/docs/brainstorms/2026-02-17-copilot-skill-naming-brainstorm.md +30 -0
  7. package/docs/plans/2026-02-14-feat-add-copilot-converter-target-plan.md +328 -0
  8. package/docs/specs/copilot.md +122 -0
  9. package/package.json +1 -1
  10. package/plugins/coding-tutor/.cursor-plugin/plugin.json +21 -0
  11. package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
  12. package/plugins/compound-engineering/.cursor-plugin/plugin.json +31 -0
  13. package/plugins/compound-engineering/.mcp.json +8 -0
  14. package/plugins/compound-engineering/CHANGELOG.md +10 -0
  15. package/plugins/compound-engineering/commands/lfg.md +3 -3
  16. package/plugins/compound-engineering/commands/slfg.md +2 -2
  17. package/plugins/compound-engineering/commands/workflows/plan.md +15 -1
  18. package/src/commands/install.ts +5 -1
  19. package/src/commands/sync.ts +8 -8
  20. package/src/converters/{claude-to-cursor.ts → claude-to-copilot.ts} +93 -49
  21. package/src/sync/{cursor.ts → copilot.ts} +36 -14
  22. package/src/targets/copilot.ts +48 -0
  23. package/src/targets/index.ts +9 -9
  24. package/src/types/copilot.ts +31 -0
  25. package/src/utils/frontmatter.ts +1 -1
  26. package/tests/copilot-converter.test.ts +467 -0
  27. package/tests/copilot-writer.test.ts +189 -0
  28. package/tests/sync-copilot.test.ts +148 -0
  29. package/src/targets/cursor.ts +0 -48
  30. package/src/types/cursor.ts +0 -29
  31. package/tests/cursor-converter.test.ts +0 -347
  32. package/tests/cursor-writer.test.ts +0 -137
  33. 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
- })