phase 03: write command md files
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
# Phase 3 Handoff Report: Write Command Files as .md
|
||||||
|
|
||||||
|
## Date
|
||||||
|
2026-02-20
|
||||||
|
|
||||||
|
## Phase
|
||||||
|
3 of feature: OpenCode Commands as .md Files, Config Merge, and Permissions Default Fix
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Implemented the `commandsDir` path resolution and command file writing in `src/targets/opencode.ts`.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Updated `src/targets/opencode.ts`
|
||||||
|
|
||||||
|
**Added `commandDir` to path resolver:**
|
||||||
|
- In global branch (line 52): Added `commandDir: path.join(outputRoot, "commands")` with inline comment
|
||||||
|
- In custom branch (line 66): Added `commandDir: path.join(outputRoot, ".opencode", "commands")` with inline comment
|
||||||
|
|
||||||
|
**Added command file writing logic (line 24-30):**
|
||||||
|
- Iterates `bundle.commandFiles`
|
||||||
|
- Writes each command as `<commandsDir>/<name>.md` with trailing newline
|
||||||
|
- Creates backup before overwriting existing files
|
||||||
|
|
||||||
|
### 2. Added tests in `tests/opencode-writer.test.ts`
|
||||||
|
|
||||||
|
- `"writes command files as .md in commands/ directory"` - Tests global-style output (`.config/opencode`)
|
||||||
|
- `"backs up existing command .md file before overwriting"` - Tests backup creation
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
```
|
||||||
|
bun test tests/opencode-writer.test.ts
|
||||||
|
6 pass, 0 fail
|
||||||
|
```
|
||||||
|
|
||||||
|
All existing tests continue to pass:
|
||||||
|
```
|
||||||
|
bun test
|
||||||
|
183 pass, 0 fail
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deliverables Complete
|
||||||
|
|
||||||
|
- [x] Updated `src/targets/opencode.ts` with commandDir path and write logic
|
||||||
|
- [x] New tests in `tests/opencode-writer.test.ts`
|
||||||
|
- [x] All tests pass
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Used `openCodePaths` instead of `paths` variable name to avoid shadowing the imported `path` module
|
||||||
|
- Command files are written with trailing newline (`content + "\n"`)
|
||||||
|
- Backup uses timestamp format `.bak.2026-02-20T...`
|
||||||
@@ -80,4 +80,50 @@ Template text here...
|
|||||||
|
|
||||||
- Converter now produces command files ready for file-system output
|
- Converter now produces command files ready for file-system output
|
||||||
- Writer phase will handle writing to `.opencode/commands/` directory
|
- Writer phase will handle writing to `.opencode/commands/` directory
|
||||||
- Phase 1 type changes are now fully utilizeds
|
- Phase 1 type changes are now fully utilizeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision: Phase 3 - Writer Writes Command .md Files
|
||||||
|
|
||||||
|
**Date:** 2026-02-20
|
||||||
|
**Status:** Implemented
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The writer needs to write command files from the bundle to the file system.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
In `src/targets/opencode.ts`:
|
||||||
|
- Add `commandDir` to return value of `resolveOpenCodePaths()` for both branches
|
||||||
|
- In `writeOpenCodeBundle()`, iterate `bundle.commandFiles` and write each as `<commandsDir>/<name>.md` with backup-before-overwrite
|
||||||
|
|
||||||
|
### Path Resolution
|
||||||
|
|
||||||
|
- Global branch (basename is "opencode" or ".opencode"): `commandsDir: path.join(outputRoot, "commands")`
|
||||||
|
- Custom branch: `commandDir: path.join(outputRoot, ".opencode", "commands")`
|
||||||
|
|
||||||
|
### Writing Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
for (const commandFile of bundle.commandFiles) {
|
||||||
|
const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`)
|
||||||
|
const cmdBackupPath = await backupFile(dest)
|
||||||
|
if (cmdBackupPath) {
|
||||||
|
console.log(`Backed up existing command file to ${cmdBackupPath}`)
|
||||||
|
}
|
||||||
|
await writeText(dest, commandFile.content + "\n")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Command files are written to `.opencode/commands/` or `commands/` directory
|
||||||
|
- Existing files are backed up before overwriting
|
||||||
|
- Files content includes trailing newline
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. Use intermediate variable for commandDir - Rejected: caused intermittent undefined errors
|
||||||
|
2. Use direct property reference `openCodePaths.commandDir` - Chosen: more reliable
|
||||||
@@ -3,29 +3,38 @@ import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/f
|
|||||||
import type { OpenCodeBundle } from "../types/opencode"
|
import type { OpenCodeBundle } from "../types/opencode"
|
||||||
|
|
||||||
export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise<void> {
|
export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise<void> {
|
||||||
const paths = resolveOpenCodePaths(outputRoot)
|
const openCodePaths = resolveOpenCodePaths(outputRoot)
|
||||||
await ensureDir(paths.root)
|
await ensureDir(openCodePaths.root)
|
||||||
|
|
||||||
const backupPath = await backupFile(paths.configPath)
|
const backupPath = await backupFile(openCodePaths.configPath)
|
||||||
if (backupPath) {
|
if (backupPath) {
|
||||||
console.log(`Backed up existing config to ${backupPath}`)
|
console.log(`Backed up existing config to ${backupPath}`)
|
||||||
}
|
}
|
||||||
await writeJson(paths.configPath, bundle.config)
|
await writeJson(openCodePaths.configPath, bundle.config)
|
||||||
|
|
||||||
const agentsDir = paths.agentsDir
|
const agentsDir = openCodePaths.agentsDir
|
||||||
for (const agent of bundle.agents) {
|
for (const agent of bundle.agents) {
|
||||||
await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
|
await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const commandFile of bundle.commandFiles) {
|
||||||
|
const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`)
|
||||||
|
const cmdBackupPath = await backupFile(dest)
|
||||||
|
if (cmdBackupPath) {
|
||||||
|
console.log(`Backed up existing command file to ${cmdBackupPath}`)
|
||||||
|
}
|
||||||
|
await writeText(dest, commandFile.content + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
if (bundle.plugins.length > 0) {
|
if (bundle.plugins.length > 0) {
|
||||||
const pluginsDir = paths.pluginsDir
|
const pluginsDir = openCodePaths.pluginsDir
|
||||||
for (const plugin of bundle.plugins) {
|
for (const plugin of bundle.plugins) {
|
||||||
await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n")
|
await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bundle.skillDirs.length > 0) {
|
if (bundle.skillDirs.length > 0) {
|
||||||
const skillsRoot = paths.skillsDir
|
const skillsRoot = openCodePaths.skillsDir
|
||||||
for (const skill of bundle.skillDirs) {
|
for (const skill of bundle.skillDirs) {
|
||||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||||
}
|
}
|
||||||
@@ -43,6 +52,8 @@ function resolveOpenCodePaths(outputRoot: string) {
|
|||||||
agentsDir: path.join(outputRoot, "agents"),
|
agentsDir: path.join(outputRoot, "agents"),
|
||||||
pluginsDir: path.join(outputRoot, "plugins"),
|
pluginsDir: path.join(outputRoot, "plugins"),
|
||||||
skillsDir: path.join(outputRoot, "skills"),
|
skillsDir: path.join(outputRoot, "skills"),
|
||||||
|
// .md command files; alternative to the command key in opencode.json
|
||||||
|
commandDir: path.join(outputRoot, "commands"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,5 +64,7 @@ function resolveOpenCodePaths(outputRoot: string) {
|
|||||||
agentsDir: path.join(outputRoot, ".opencode", "agents"),
|
agentsDir: path.join(outputRoot, ".opencode", "agents"),
|
||||||
pluginsDir: path.join(outputRoot, ".opencode", "plugins"),
|
pluginsDir: path.join(outputRoot, ".opencode", "plugins"),
|
||||||
skillsDir: path.join(outputRoot, ".opencode", "skills"),
|
skillsDir: path.join(outputRoot, ".opencode", "skills"),
|
||||||
|
// .md command files; alternative to the command key in opencode.json
|
||||||
|
commandDir: path.join(outputRoot, ".opencode", "commands"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,4 +120,56 @@ describe("writeOpenCodeBundle", () => {
|
|||||||
const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8"))
|
const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8"))
|
||||||
expect(backupContent.custom).toBe("value")
|
expect(backupContent.custom).toBe("value")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("writes command files as .md in commands/ directory", async () => {
|
||||||
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cmd-"))
|
||||||
|
const outputRoot = path.join(tempRoot, ".config", "opencode")
|
||||||
|
const bundle: OpenCodeBundle = {
|
||||||
|
config: { $schema: "https://opencode.ai/config.json" },
|
||||||
|
agents: [],
|
||||||
|
plugins: [],
|
||||||
|
commandFiles: [{ name: "my-cmd", content: "---\ndescription: Test\n---\n\nDo something." }],
|
||||||
|
skillDirs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeOpenCodeBundle(outputRoot, bundle)
|
||||||
|
|
||||||
|
const cmdPath = path.join(outputRoot, "commands", "my-cmd.md")
|
||||||
|
expect(await exists(cmdPath)).toBe(true)
|
||||||
|
|
||||||
|
const content = await fs.readFile(cmdPath, "utf8")
|
||||||
|
expect(content).toBe("---\ndescription: Test\n---\n\nDo something.\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("backs up existing command .md file before overwriting", async () => {
|
||||||
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cmd-backup-"))
|
||||||
|
const outputRoot = path.join(tempRoot, ".opencode")
|
||||||
|
const commandsDir = path.join(outputRoot, "commands")
|
||||||
|
await fs.mkdir(commandsDir, { recursive: true })
|
||||||
|
|
||||||
|
const cmdPath = path.join(commandsDir, "my-cmd.md")
|
||||||
|
await fs.writeFile(cmdPath, "old content\n")
|
||||||
|
|
||||||
|
const bundle: OpenCodeBundle = {
|
||||||
|
config: { $schema: "https://opencode.ai/config.json" },
|
||||||
|
agents: [],
|
||||||
|
plugins: [],
|
||||||
|
commandFiles: [{ name: "my-cmd", content: "---\ndescription: New\n---\n\nNew content." }],
|
||||||
|
skillDirs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeOpenCodeBundle(outputRoot, bundle)
|
||||||
|
|
||||||
|
// New content should be written
|
||||||
|
const content = await fs.readFile(cmdPath, "utf8")
|
||||||
|
expect(content).toBe("---\ndescription: New\n---\n\nNew content.\n")
|
||||||
|
|
||||||
|
// Backup should exist
|
||||||
|
const files = await fs.readdir(commandsDir)
|
||||||
|
const backupFileName = files.find((f) => f.startsWith("my-cmd.md.bak."))
|
||||||
|
expect(backupFileName).toBeDefined()
|
||||||
|
|
||||||
|
const backupContent = await fs.readFile(path.join(commandsDir, backupFileName!), "utf8")
|
||||||
|
expect(backupContent).toBe("old content\n")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user