feat: add OpenCode/Codex outputs and update changelog (#104)
* Add OpenCode converter coverage and specs * Add Codex target support and spec docs * Generate Codex command skills and refresh spec docs * Add global Codex install path * fix: harden plugin path loading and codex descriptions * feat: ensure codex agents block on convert/install * docs: clarify target branch usage for review * chore: prep npm package metadata and release notes * docs: mention opencode and codex in changelog * docs: update CLI usage and remove stale todos * feat: install from GitHub with global outputs
This commit is contained in:
91
src/targets/codex.ts
Normal file
91
src/targets/codex.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import path from "path"
|
||||
import { copyDir, ensureDir, writeText } from "../utils/files"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
|
||||
export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise<void> {
|
||||
const codexRoot = resolveCodexRoot(outputRoot)
|
||||
await ensureDir(codexRoot)
|
||||
|
||||
if (bundle.prompts.length > 0) {
|
||||
const promptsDir = path.join(codexRoot, "prompts")
|
||||
for (const prompt of bundle.prompts) {
|
||||
await writeText(path.join(promptsDir, `${prompt.name}.md`), prompt.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
const skillsRoot = path.join(codexRoot, "skills")
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.generatedSkills.length > 0) {
|
||||
const skillsRoot = path.join(codexRoot, "skills")
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(skillsRoot, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
const config = renderCodexConfig(bundle.mcpServers)
|
||||
if (config) {
|
||||
await writeText(path.join(codexRoot, "config.toml"), config)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodexRoot(outputRoot: string): string {
|
||||
return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex")
|
||||
}
|
||||
|
||||
export function renderCodexConfig(mcpServers?: Record<string, ClaudeMcpServer>): string | null {
|
||||
if (!mcpServers || Object.keys(mcpServers).length === 0) return null
|
||||
|
||||
const lines: string[] = ["# Generated by compound-plugin", ""]
|
||||
|
||||
for (const [name, server] of Object.entries(mcpServers)) {
|
||||
const key = formatTomlKey(name)
|
||||
lines.push(`[mcp_servers.${key}]`)
|
||||
|
||||
if (server.command) {
|
||||
lines.push(`command = ${formatTomlString(server.command)}`)
|
||||
if (server.args && server.args.length > 0) {
|
||||
const args = server.args.map((arg) => formatTomlString(arg)).join(", ")
|
||||
lines.push(`args = [${args}]`)
|
||||
}
|
||||
|
||||
if (server.env && Object.keys(server.env).length > 0) {
|
||||
lines.push("")
|
||||
lines.push(`[mcp_servers.${key}.env]`)
|
||||
for (const [envKey, value] of Object.entries(server.env)) {
|
||||
lines.push(`${formatTomlKey(envKey)} = ${formatTomlString(value)}`)
|
||||
}
|
||||
}
|
||||
} else if (server.url) {
|
||||
lines.push(`url = ${formatTomlString(server.url)}`)
|
||||
if (server.headers && Object.keys(server.headers).length > 0) {
|
||||
lines.push(`http_headers = ${formatTomlInlineTable(server.headers)}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function formatTomlString(value: string): string {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function formatTomlKey(value: string): string {
|
||||
if (/^[A-Za-z0-9_-]+$/.test(value)) return value
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function formatTomlInlineTable(entries: Record<string, string>): string {
|
||||
const parts = Object.entries(entries).map(
|
||||
([key, value]) => `${formatTomlKey(key)} = ${formatTomlString(value)}`,
|
||||
)
|
||||
return `{ ${parts.join(", ")} }`
|
||||
}
|
||||
29
src/targets/index.ts
Normal file
29
src/targets/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ClaudePlugin } from "../types/claude"
|
||||
import type { OpenCodeBundle } from "../types/opencode"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
||||
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
||||
import { writeOpenCodeBundle } from "./opencode"
|
||||
import { writeCodexBundle } from "./codex"
|
||||
|
||||
export type TargetHandler<TBundle = unknown> = {
|
||||
name: string
|
||||
implemented: boolean
|
||||
convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
|
||||
write: (outputRoot: string, bundle: TBundle) => Promise<void>
|
||||
}
|
||||
|
||||
export const targets: Record<string, TargetHandler> = {
|
||||
opencode: {
|
||||
name: "opencode",
|
||||
implemented: true,
|
||||
convert: convertClaudeToOpenCode,
|
||||
write: writeOpenCodeBundle,
|
||||
},
|
||||
codex: {
|
||||
name: "codex",
|
||||
implemented: true,
|
||||
convert: convertClaudeToCodex as TargetHandler<CodexBundle>["convert"],
|
||||
write: writeCodexBundle as TargetHandler<CodexBundle>["write"],
|
||||
},
|
||||
}
|
||||
48
src/targets/opencode.ts
Normal file
48
src/targets/opencode.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import path from "path"
|
||||
import { copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
||||
import type { OpenCodeBundle } from "../types/opencode"
|
||||
|
||||
export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise<void> {
|
||||
const paths = resolveOpenCodePaths(outputRoot)
|
||||
await ensureDir(paths.root)
|
||||
await writeJson(paths.configPath, bundle.config)
|
||||
|
||||
const agentsDir = paths.agentsDir
|
||||
for (const agent of bundle.agents) {
|
||||
await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
|
||||
}
|
||||
|
||||
if (bundle.plugins.length > 0) {
|
||||
const pluginsDir = paths.pluginsDir
|
||||
for (const plugin of bundle.plugins) {
|
||||
await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
const skillsRoot = paths.skillsDir
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOpenCodePaths(outputRoot: string) {
|
||||
if (path.basename(outputRoot) === ".opencode") {
|
||||
return {
|
||||
root: outputRoot,
|
||||
configPath: path.join(outputRoot, "opencode.json"),
|
||||
agentsDir: path.join(outputRoot, "agents"),
|
||||
pluginsDir: path.join(outputRoot, "plugins"),
|
||||
skillsDir: path.join(outputRoot, "skills"),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: outputRoot,
|
||||
configPath: path.join(outputRoot, "opencode.json"),
|
||||
agentsDir: path.join(outputRoot, ".opencode", "agents"),
|
||||
pluginsDir: path.join(outputRoot, ".opencode", "plugins"),
|
||||
skillsDir: path.join(outputRoot, ".opencode", "skills"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user