@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.
- package/.cursor-plugin/marketplace.json +25 -0
- package/CHANGELOG.md +13 -0
- package/README.md +14 -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/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/install.ts +5 -1
- package/src/commands/sync.ts +8 -8
- package/src/converters/{claude-to-cursor.ts → claude-to-copilot.ts} +93 -49
- package/src/sync/{cursor.ts → copilot.ts} +36 -14
- package/src/targets/copilot.ts +48 -0
- package/src/targets/index.ts +9 -9
- package/src/types/copilot.ts +31 -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/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
|
@@ -4,19 +4,21 @@ import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
|
4
4
|
import type { ClaudeMcpServer } from "../types/claude"
|
|
5
5
|
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
6
6
|
|
|
7
|
-
type
|
|
7
|
+
type CopilotMcpServer = {
|
|
8
|
+
type: string
|
|
8
9
|
command?: string
|
|
9
10
|
args?: string[]
|
|
10
11
|
url?: string
|
|
12
|
+
tools: string[]
|
|
11
13
|
env?: Record<string, string>
|
|
12
14
|
headers?: Record<string, string>
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
type
|
|
16
|
-
mcpServers: Record<string,
|
|
17
|
+
type CopilotMcpConfig = {
|
|
18
|
+
mcpServers: Record<string, CopilotMcpServer>
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export async function
|
|
21
|
+
export async function syncToCopilot(
|
|
20
22
|
config: ClaudeHomeConfig,
|
|
21
23
|
outputRoot: string,
|
|
22
24
|
): Promise<void> {
|
|
@@ -33,10 +35,10 @@ export async function syncToCursor(
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (Object.keys(config.mcpServers).length > 0) {
|
|
36
|
-
const mcpPath = path.join(outputRoot, "mcp.json")
|
|
38
|
+
const mcpPath = path.join(outputRoot, "copilot-mcp-config.json")
|
|
37
39
|
const existing = await readJsonSafe(mcpPath)
|
|
38
|
-
const converted =
|
|
39
|
-
const merged:
|
|
40
|
+
const converted = convertMcpForCopilot(config.mcpServers)
|
|
41
|
+
const merged: CopilotMcpConfig = {
|
|
40
42
|
mcpServers: {
|
|
41
43
|
...(existing.mcpServers ?? {}),
|
|
42
44
|
...converted,
|
|
@@ -46,10 +48,10 @@ export async function syncToCursor(
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
async function readJsonSafe(filePath: string): Promise<Partial<
|
|
51
|
+
async function readJsonSafe(filePath: string): Promise<Partial<CopilotMcpConfig>> {
|
|
50
52
|
try {
|
|
51
53
|
const content = await fs.readFile(filePath, "utf-8")
|
|
52
|
-
return JSON.parse(content) as Partial<
|
|
54
|
+
return JSON.parse(content) as Partial<CopilotMcpConfig>
|
|
53
55
|
} catch (err) {
|
|
54
56
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
55
57
|
return {}
|
|
@@ -58,21 +60,41 @@ async function readJsonSafe(filePath: string): Promise<Partial<CursorMcpConfig>>
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
function
|
|
63
|
+
function convertMcpForCopilot(
|
|
62
64
|
servers: Record<string, ClaudeMcpServer>,
|
|
63
|
-
): Record<string,
|
|
64
|
-
const result: Record<string,
|
|
65
|
+
): Record<string, CopilotMcpServer> {
|
|
66
|
+
const result: Record<string, CopilotMcpServer> = {}
|
|
65
67
|
for (const [name, server] of Object.entries(servers)) {
|
|
66
|
-
const entry:
|
|
68
|
+
const entry: CopilotMcpServer = {
|
|
69
|
+
type: server.command ? "local" : "sse",
|
|
70
|
+
tools: ["*"],
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
if (server.command) {
|
|
68
74
|
entry.command = server.command
|
|
69
75
|
if (server.args && server.args.length > 0) entry.args = server.args
|
|
70
|
-
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
|
|
71
76
|
} else if (server.url) {
|
|
72
77
|
entry.url = server.url
|
|
73
78
|
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
|
|
74
79
|
}
|
|
80
|
+
|
|
81
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
82
|
+
entry.env = prefixEnvVars(server.env)
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
result[name] = entry
|
|
76
86
|
}
|
|
77
87
|
return result
|
|
78
88
|
}
|
|
89
|
+
|
|
90
|
+
function prefixEnvVars(env: Record<string, string>): Record<string, string> {
|
|
91
|
+
const result: Record<string, string> = {}
|
|
92
|
+
for (const [key, value] of Object.entries(env)) {
|
|
93
|
+
if (key.startsWith("COPILOT_MCP_")) {
|
|
94
|
+
result[key] = value
|
|
95
|
+
} else {
|
|
96
|
+
result[`COPILOT_MCP_${key}`] = value
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { CopilotBundle } from "../types/copilot"
|
|
4
|
+
|
|
5
|
+
export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise<void> {
|
|
6
|
+
const paths = resolveCopilotPaths(outputRoot)
|
|
7
|
+
await ensureDir(paths.githubDir)
|
|
8
|
+
|
|
9
|
+
if (bundle.agents.length > 0) {
|
|
10
|
+
const agentsDir = path.join(paths.githubDir, "agents")
|
|
11
|
+
for (const agent of bundle.agents) {
|
|
12
|
+
await writeText(path.join(agentsDir, `${agent.name}.agent.md`), agent.content + "\n")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (bundle.generatedSkills.length > 0) {
|
|
17
|
+
const skillsDir = path.join(paths.githubDir, "skills")
|
|
18
|
+
for (const skill of bundle.generatedSkills) {
|
|
19
|
+
await writeText(path.join(skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bundle.skillDirs.length > 0) {
|
|
24
|
+
const skillsDir = path.join(paths.githubDir, "skills")
|
|
25
|
+
for (const skill of bundle.skillDirs) {
|
|
26
|
+
await copyDir(skill.sourceDir, path.join(skillsDir, skill.name))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (bundle.mcpConfig && Object.keys(bundle.mcpConfig).length > 0) {
|
|
31
|
+
const mcpPath = path.join(paths.githubDir, "copilot-mcp-config.json")
|
|
32
|
+
const backupPath = await backupFile(mcpPath)
|
|
33
|
+
if (backupPath) {
|
|
34
|
+
console.log(`Backed up existing copilot-mcp-config.json to ${backupPath}`)
|
|
35
|
+
}
|
|
36
|
+
await writeJson(mcpPath, { mcpServers: bundle.mcpConfig })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveCopilotPaths(outputRoot: string) {
|
|
41
|
+
const base = path.basename(outputRoot)
|
|
42
|
+
// If already pointing at .github, write directly into it
|
|
43
|
+
if (base === ".github") {
|
|
44
|
+
return { githubDir: outputRoot }
|
|
45
|
+
}
|
|
46
|
+
// Otherwise nest under .github
|
|
47
|
+
return { githubDir: path.join(outputRoot, ".github") }
|
|
48
|
+
}
|
package/src/targets/index.ts
CHANGED
|
@@ -2,20 +2,20 @@ import type { ClaudePlugin } from "../types/claude"
|
|
|
2
2
|
import type { OpenCodeBundle } from "../types/opencode"
|
|
3
3
|
import type { CodexBundle } from "../types/codex"
|
|
4
4
|
import type { DroidBundle } from "../types/droid"
|
|
5
|
-
import type { CursorBundle } from "../types/cursor"
|
|
6
5
|
import type { PiBundle } from "../types/pi"
|
|
6
|
+
import type { CopilotBundle } from "../types/copilot"
|
|
7
7
|
import type { GeminiBundle } from "../types/gemini"
|
|
8
8
|
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
|
9
9
|
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
|
10
10
|
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
|
11
|
-
import { convertClaudeToCursor } from "../converters/claude-to-cursor"
|
|
12
11
|
import { convertClaudeToPi } from "../converters/claude-to-pi"
|
|
12
|
+
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
|
13
13
|
import { convertClaudeToGemini } from "../converters/claude-to-gemini"
|
|
14
14
|
import { writeOpenCodeBundle } from "./opencode"
|
|
15
15
|
import { writeCodexBundle } from "./codex"
|
|
16
16
|
import { writeDroidBundle } from "./droid"
|
|
17
|
-
import { writeCursorBundle } from "./cursor"
|
|
18
17
|
import { writePiBundle } from "./pi"
|
|
18
|
+
import { writeCopilotBundle } from "./copilot"
|
|
19
19
|
import { writeGeminiBundle } from "./gemini"
|
|
20
20
|
|
|
21
21
|
export type TargetHandler<TBundle = unknown> = {
|
|
@@ -44,18 +44,18 @@ export const targets: Record<string, TargetHandler> = {
|
|
|
44
44
|
convert: convertClaudeToDroid as TargetHandler<DroidBundle>["convert"],
|
|
45
45
|
write: writeDroidBundle as TargetHandler<DroidBundle>["write"],
|
|
46
46
|
},
|
|
47
|
-
cursor: {
|
|
48
|
-
name: "cursor",
|
|
49
|
-
implemented: true,
|
|
50
|
-
convert: convertClaudeToCursor as TargetHandler<CursorBundle>["convert"],
|
|
51
|
-
write: writeCursorBundle as TargetHandler<CursorBundle>["write"],
|
|
52
|
-
},
|
|
53
47
|
pi: {
|
|
54
48
|
name: "pi",
|
|
55
49
|
implemented: true,
|
|
56
50
|
convert: convertClaudeToPi as TargetHandler<PiBundle>["convert"],
|
|
57
51
|
write: writePiBundle as TargetHandler<PiBundle>["write"],
|
|
58
52
|
},
|
|
53
|
+
copilot: {
|
|
54
|
+
name: "copilot",
|
|
55
|
+
implemented: true,
|
|
56
|
+
convert: convertClaudeToCopilot as TargetHandler<CopilotBundle>["convert"],
|
|
57
|
+
write: writeCopilotBundle as TargetHandler<CopilotBundle>["write"],
|
|
58
|
+
},
|
|
59
59
|
gemini: {
|
|
60
60
|
name: "gemini",
|
|
61
61
|
implemented: true,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CopilotAgent = {
|
|
2
|
+
name: string
|
|
3
|
+
content: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CopilotGeneratedSkill = {
|
|
7
|
+
name: string
|
|
8
|
+
content: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CopilotSkillDir = {
|
|
12
|
+
name: string
|
|
13
|
+
sourceDir: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CopilotMcpServer = {
|
|
17
|
+
type: string
|
|
18
|
+
command?: string
|
|
19
|
+
args?: string[]
|
|
20
|
+
url?: string
|
|
21
|
+
tools: string[]
|
|
22
|
+
env?: Record<string, string>
|
|
23
|
+
headers?: Record<string, string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CopilotBundle = {
|
|
27
|
+
agents: CopilotAgent[]
|
|
28
|
+
generatedSkills: CopilotGeneratedSkill[]
|
|
29
|
+
skillDirs: CopilotSkillDir[]
|
|
30
|
+
mcpConfig?: Record<string, CopilotMcpServer>
|
|
31
|
+
}
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -58,7 +58,7 @@ function formatYamlValue(value: unknown): string {
|
|
|
58
58
|
if (raw.includes("\n")) {
|
|
59
59
|
return `|\n${raw.split("\n").map((line) => ` ${line}`).join("\n")}`
|
|
60
60
|
}
|
|
61
|
-
if (raw.includes(":") || raw.startsWith("[") || raw.startsWith("{")) {
|
|
61
|
+
if (raw.includes(":") || raw.startsWith("[") || raw.startsWith("{") || raw === "*") {
|
|
62
62
|
return JSON.stringify(raw)
|
|
63
63
|
}
|
|
64
64
|
return raw
|