diff --git a/.opencode/plugins/ponytail-frontmatter.cjs b/.opencode/plugins/ponytail-frontmatter.cjs new file mode 100644 index 0000000..1145929 --- /dev/null +++ b/.opencode/plugins/ponytail-frontmatter.cjs @@ -0,0 +1,23 @@ +'use strict'; + +// ponytail command-file frontmatter parser. +// +// Pulled out of ponytail.mjs so the plugin module's only top-level export is +// the plugin function itself. OpenCode's legacy plugin loader (the one that +// runs before v1 plugins are detected) treats every function exported from a +// plugin module as a plugin; calling the frontmatter parser as one threw +// "path must be a string or a file descriptor" because it got the plugin +// context object as its first argument. Keeping the parser in its own module +// leaves exactly one plugin-shaped export on ponytail.mjs. + +function parseCommandFile(filePath) { + const fs = require('fs'); + const content = fs.readFileSync(filePath, 'utf8'); + // Tolerate CRLF: a Windows checkout (autocrlf) delivers \r\n, npm ships \n. + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + if (!match) return null; + const description = match[1].match(/description:\s*(.+)/)?.[1]?.trim(); + return { description, template: match[2].trim() }; +} + +module.exports = { parseCommandFile }; diff --git a/.opencode/plugins/ponytail.mjs b/.opencode/plugins/ponytail.mjs index 4db6f4a..6a77468 100644 --- a/.opencode/plugins/ponytail.mjs +++ b/.opencode/plugins/ponytail.mjs @@ -21,6 +21,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const { getPonytailInstructions } = require('../../hooks/ponytail-instructions'); const { getDefaultMode, normalizePersistedMode } = require('../../hooks/ponytail-config'); +const { parseCommandFile } = require('./ponytail-frontmatter.cjs'); // OpenCode has no flag-file convention of its own; keep mode beside its config. const statePath = path.join( @@ -42,15 +43,6 @@ function writeMode(mode) { fs.writeFileSync(statePath, mode); } -export function parseCommandFile(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - // Tolerate CRLF: a Windows checkout (autocrlf) delivers \r\n, npm ships \n. - const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); - if (!match) return null; - const description = match[1].match(/description:\s*(.+)/)?.[1]?.trim(); - return { description, template: match[2].trim() }; -} - export default async ({ client } = {}) => { const log = (level, message) => { try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {} diff --git a/tests/opencode-plugin.test.js b/tests/opencode-plugin.test.js index bb5b8ca..d598643 100644 --- a/tests/opencode-plugin.test.js +++ b/tests/opencode-plugin.test.js @@ -23,7 +23,11 @@ test.before(async () => { const url = pathToFileURL(path.join(__dirname, '..', '.opencode', 'plugins', 'ponytail.mjs')); const mod = await import(url); loadPlugin = mod.default; - parseCommandFile = mod.parseCommandFile; + // The frontmatter parser used to be exported from the plugin module itself. + // OpenCode's legacy loader treats every exported function as a plugin and + // tried to invoke it with the plugin context object, which crashed. The + // parser now lives in its own .cjs sibling; require it directly. + parseCommandFile = require(path.join(__dirname, '..', '.opencode', 'plugins', 'ponytail-frontmatter.cjs')).parseCommandFile; }); function transform(hooks) {