phase 01: type change for command files
This commit is contained in:
@@ -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<string, OpenCodeCommandConfig>` 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
|
||||
@@ -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: <name>.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
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from "../types/claude"
|
||||
import type {
|
||||
OpenCodeBundle,
|
||||
OpenCodeCommandConfig,
|
||||
OpenCodeCommandFile,
|
||||
OpenCodeConfig,
|
||||
OpenCodeMcpServer,
|
||||
} from "../types/opencode"
|
||||
|
||||
@@ -7,7 +7,6 @@ export type OpenCodeConfig = {
|
||||
tools?: Record<string, boolean>
|
||||
permission?: Record<string, OpenCodePermission | Record<string, OpenCodePermission>>
|
||||
agent?: Record<string, OpenCodeAgentConfig>
|
||||
command?: Record<string, OpenCodeCommandConfig>
|
||||
mcp?: Record<string, OpenCodeMcpServer>
|
||||
}
|
||||
|
||||
@@ -20,13 +19,6 @@ export type OpenCodeAgentConfig = {
|
||||
permission?: Record<string, OpenCodePermission>
|
||||
}
|
||||
|
||||
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 }[]
|
||||
}
|
||||
|
||||
@@ -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<string, string | Record<string, string>>
|
||||
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", () => {
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user