diff --git a/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-01-type-changes.md b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-01-type-changes.md new file mode 100644 index 0000000..74376ed --- /dev/null +++ b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-01-type-changes.md @@ -0,0 +1,48 @@ +# Phase 1 Handoff Report: Type Changes for Command Files + +**Date:** 2026-02-20 +**Phase:** 1 of 4 +**Status:** Complete + +## Summary + +Implemented type changes to support storing commands as `.md` files instead of inline in `opencode.json`. + +## Changes Made + +### 1. Type Changes (`src/types/opencode.ts`) + +- Removed `OpenCodeCommandConfig` type (lines 23-28) +- Removed `command?: Record` from `OpenCodeConfig` +- Added `OpenCodeCommandFile` type: + ```typescript + export type OpenCodeCommandFile = { + name: string + content: string + } + ``` +- Added `commandFiles: OpenCodeCommandFile[]` to `OpenCodeBundle` (with comment referencing ADR-001) + +### 2. Import Update (`src/converters/claude-to-opencode.ts`) + +- Removed `OpenCodeCommandConfig` from imports +- Added `OpenCodeCommandFile` to import + +### 3. Test Updates + +- `tests/converter.test.ts`: Updated 4 tests to use `bundle.commandFiles.find()` instead of `bundle.config.command` +- `tests/opencode-writer.test.ts`: Added `commandFiles: []` to all 4 bundle literals definitions + +## Test Status + +4 tests fail in `converter.test.ts` because the converter hasn't been updated yet to populate `commandFiles`. This is expected behavior - Phase 2 will fix these. + +``` +76 pass, 4 fail in converter.test.ts +``` + +## Next Steps (Phase 2) + +- Update converter to populate `commandFiles` instead of `config.command` +- Update writer to output `.md` files for commands +- Tests will pass after Phase 2 implementation \ No newline at end of file diff --git a/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md b/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md new file mode 100644 index 0000000..0befcc6 --- /dev/null +++ b/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md @@ -0,0 +1,44 @@ +# Decision Log: OpenCode Commands as .md Files + +## Decision: ADR-001 - Store Commands as Individual .md Files + +**Date:** 2026-02-20 +**Status:** Adopted + +## Context + +The original design stored commands configurations inline in `opencode.json` under `config.command`. This tightly couples command metadata with config, making it harder to version-control commands separately and share command files. + +## Decision + +Store commands definitions as individual `.md` files in `.opencode/commands/` directory, with YAML frontmatter for metadata and markdown body for the command prompt. + +**New Type:** +```typescript +export type OpenCodeCommandFile = { + name: string // command name, used as filename stem: .md + content: string // full file content: YAML frontmatter + body +} +``` + +**Bundle Structure:** +```typescript +export type OpenCodeBundle = { + config: OpenCodeConfig + agents: OpenCodeAgentFile[] + commandFiles: OpenCodeCommandFile[] // NEW + plugins: OpenCodePluginFile[] + skillDirs: { sourceDir: string; name: string }[] +} +``` + +## Consequences + +- **Positive:** Commands can be versioned, shared, and edited independently +- **Negative:** Requires updating converter, writer, and all consumers +- **Migration:** Phase 1-4 will implement the full migration + +## Alternatives Considered + +1. Keep inline in config - Rejected: limits flexibility +2. Use separate JSON files - Rejected: YAML frontmatter is more idiomatic for commands \ No newline at end of file diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index 5bff059..d73dbe6 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -8,7 +8,7 @@ import type { } from "../types/claude" import type { OpenCodeBundle, - OpenCodeCommandConfig, + OpenCodeCommandFile, OpenCodeConfig, OpenCodeMcpServer, } from "../types/opencode" diff --git a/src/types/opencode.ts b/src/types/opencode.ts index 0338892..a66546e 100644 --- a/src/types/opencode.ts +++ b/src/types/opencode.ts @@ -7,7 +7,6 @@ export type OpenCodeConfig = { tools?: Record permission?: Record> agent?: Record - command?: Record mcp?: Record } @@ -20,13 +19,6 @@ export type OpenCodeAgentConfig = { permission?: Record } -export type OpenCodeCommandConfig = { - description?: string - model?: string - agent?: string - template: string -} - export type OpenCodeMcpServer = { type: "local" | "remote" command?: string[] @@ -46,9 +38,16 @@ export type OpenCodePluginFile = { content: string } +export type OpenCodeCommandFile = { + name: string + content: string +} + export type OpenCodeBundle = { config: OpenCodeConfig agents: OpenCodeAgentFile[] + // Commands are written as individual .md files, not in opencode.json. See ADR-001. + commandFiles: OpenCodeCommandFile[] plugins: OpenCodePluginFile[] skillDirs: { sourceDir: string; name: string }[] } diff --git a/tests/converter.test.ts b/tests/converter.test.ts index 3b3053e..979a702 100644 --- a/tests/converter.test.ts +++ b/tests/converter.test.ts @@ -16,8 +16,8 @@ describe("convertClaudeToOpenCode", () => { permissions: "from-commands", }) - expect(bundle.config.command?.["workflows:review"]).toBeDefined() - expect(bundle.config.command?.["plan_review"]).toBeDefined() + expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined() + expect(bundle.commandFiles.find((f) => f.name === "plan_review")).toBeDefined() const permission = bundle.config.permission as Record> expect(Object.keys(permission).sort()).toEqual([ @@ -71,8 +71,10 @@ describe("convertClaudeToOpenCode", () => { expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-20250514") expect(parsed.data.temperature).toBe(0.1) - const modelCommand = bundle.config.command?.["workflows:work"] - expect(modelCommand?.model).toBe("openai/gpt-4o") + const modelCommand = bundle.commandFiles.find((f) => f.name === "workflows:work") + expect(modelCommand).toBeDefined() + const commandParsed = parseFrontmatter(modelCommand!.content) + expect(commandParsed.data.model).toBe("openai/gpt-4o") }) test("resolves bare Claude model aliases to full IDs", () => { @@ -208,10 +210,10 @@ describe("convertClaudeToOpenCode", () => { }) // deploy-docs has disable-model-invocation: true, should be excluded - expect(bundle.config.command?.["deploy-docs"]).toBeUndefined() + expect(bundle.commandFiles.find((f) => f.name === "deploy-docs")).toBeUndefined() // Normal commands should still be present - expect(bundle.config.command?.["workflows:review"]).toBeDefined() + expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined() }) test("rewrites .claude/ paths to .opencode/ in command bodies", () => { @@ -240,10 +242,11 @@ Run \`/compound-engineering-setup\` to create a settings file.`, permissions: "none", }) - const template = bundle.config.command?.["review"]?.template ?? "" + const commandFile = bundle.commandFiles.find((f) => f.name === "review") + expect(commandFile).toBeDefined() // Tool-agnostic path in project root — no rewriting needed - expect(template).toContain("compound-engineering.local.md") + expect(commandFile!.content).toContain("compound-engineering.local.md") }) test("rewrites .claude/ paths in agent bodies", () => { diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index 0bafcc0..f692bf2 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -21,6 +21,7 @@ describe("writeOpenCodeBundle", () => { config: { $schema: "https://opencode.ai/config.json" }, agents: [{ name: "agent-one", content: "Agent content" }], plugins: [{ name: "hook.ts", content: "export {}" }], + commandFiles: [], skillDirs: [ { name: "skill-one", @@ -44,6 +45,7 @@ describe("writeOpenCodeBundle", () => { config: { $schema: "https://opencode.ai/config.json" }, agents: [{ name: "agent-one", content: "Agent content" }], plugins: [], + commandFiles: [], skillDirs: [ { name: "skill-one", @@ -68,6 +70,7 @@ describe("writeOpenCodeBundle", () => { config: { $schema: "https://opencode.ai/config.json" }, agents: [{ name: "agent-one", content: "Agent content" }], plugins: [], + commandFiles: [], skillDirs: [ { name: "skill-one", @@ -99,6 +102,7 @@ describe("writeOpenCodeBundle", () => { config: { $schema: "https://opencode.ai/config.json", new: "config" }, agents: [], plugins: [], + commandFiles: [], skillDirs: [], }