From 5abddbcbd9262ea40e375665575b0a8bfce000e2 Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 20 Feb 2026 13:28:25 -0500 Subject: [PATCH] phase 03: write command md files --- ...2026-02-20-phase-03-write-command-files.md | 54 +++++++++++++++++++ .../decisions.md | 48 ++++++++++++++++- src/targets/opencode.ts | 29 +++++++--- tests/opencode-writer.test.ts | 52 ++++++++++++++++++ 4 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-03-write-command-files.md diff --git a/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-03-write-command-files.md b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-03-write-command-files.md new file mode 100644 index 0000000..84fc3e3 --- /dev/null +++ b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-03-write-command-files.md @@ -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 `/.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...` \ 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 index 622c8b3..eb602b9 100644 --- a/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md +++ b/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md @@ -80,4 +80,50 @@ Template text here... - Converter now produces command files ready for file-system output - Writer phase will handle writing to `.opencode/commands/` directory -- Phase 1 type changes are now fully utilizeds \ No newline at end of file +- 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 `/.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 \ No newline at end of file diff --git a/src/targets/opencode.ts b/src/targets/opencode.ts index 24e8faf..3f9b80d 100644 --- a/src/targets/opencode.ts +++ b/src/targets/opencode.ts @@ -3,29 +3,38 @@ import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/f import type { OpenCodeBundle } from "../types/opencode" export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise { - const paths = resolveOpenCodePaths(outputRoot) - await ensureDir(paths.root) + const openCodePaths = resolveOpenCodePaths(outputRoot) + await ensureDir(openCodePaths.root) - const backupPath = await backupFile(paths.configPath) + const backupPath = await backupFile(openCodePaths.configPath) if (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) { 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) { - const pluginsDir = paths.pluginsDir + const pluginsDir = openCodePaths.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 + const skillsRoot = openCodePaths.skillsDir for (const skill of bundle.skillDirs) { await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name)) } @@ -43,6 +52,8 @@ function resolveOpenCodePaths(outputRoot: string) { agentsDir: path.join(outputRoot, "agents"), pluginsDir: path.join(outputRoot, "plugins"), 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"), pluginsDir: path.join(outputRoot, ".opencode", "plugins"), skillsDir: path.join(outputRoot, ".opencode", "skills"), + // .md command files; alternative to the command key in opencode.json + commandDir: path.join(outputRoot, ".opencode", "commands"), } -} +} \ No newline at end of file diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index f692bf2..e017437 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -120,4 +120,56 @@ describe("writeOpenCodeBundle", () => { const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8")) 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") + }) })