fix(opencode): stop exporting parseCommandFile from plugin module (#301)

OpenCode's legacy plugin loader iterates every exported function in a
plugin module and tries to invoke each one with the plugin context object.
parseCommandFile was exported alongside the default plugin function, so
the loader called it with an object instead of a file path and crashed
with 'path must be a string or a file descriptor' before any hook ran,
which aborted the whole plugin load.

Move the frontmatter parser to its own .cjs sibling; the plugin module
now exposes exactly one top-level export (the default plugin function).
The plugin requires the parser via createRequire, and the existing
parseCommandFile tests now require the sibling directly.

Co-authored-by: ayushere <ayushere@users.noreply.github.com>
This commit is contained in:
D3VL0PR 2026-07-09 15:14:31 +05:30 committed by GitHub
parent 8e69b4a55f
commit 511ddd4d62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 29 additions and 10 deletions

View File

@ -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 };

View File

@ -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) {}

View File

@ -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) {