feat: publish ponytail as an installable npm package for OpenCode and Pi (#197)

* feat: register slash commands from .opencode/command/*.md

Add parseCommandFile() to read frontmatter-described markdown files
and wire them into opencode's config.command during init.

Extend the config hook to scan .opencode/command/ and register each
.md file as a named slash command.

Update the plugin doc comment to reflect the npm install path
(opencode-ponytail) vs the old relative path.

* chore: rename package to opencode-ponytail

Align package name with npm convention for opencode plugins.
Update keywords to include opencode-plugin and opencode tags.
Fix description back to original correct wording (grammatical
regression introduced during editing).

* chore: add npm metadata and publish workflow

Add author, homepage, repository, bugs, main, exports, files, and
publishConfig fields to package.json for npm publishing.

Add .github/workflows/publish.yml to auto-publish to npm on version
tags (v*) with provenance.

* docs: add npm plugin install for opencode-ponytail

---------

Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
This commit is contained in:
Dang Trung An
2026-06-24 02:09:40 +02:00
committed by GitHub
co-authored by Emeriko
parent 2b426c6ac9
commit 17a466013e
5 changed files with 107 additions and 9 deletions
+21 -2
View File
@@ -18,10 +18,12 @@ process.env.XDG_CONFIG_HOME = tmp;
delete process.env.PONYTAIL_DEFAULT_MODE;
const statePath = path.join(tmp, 'opencode', '.ponytail-active');
let loadPlugin;
let loadPlugin, parseCommandFile;
test.before(async () => {
const url = pathToFileURL(path.join(__dirname, '..', '.opencode', 'plugins', 'ponytail.mjs'));
loadPlugin = (await import(url)).default;
const mod = await import(url);
loadPlugin = mod.default;
parseCommandFile = mod.parseCommandFile;
});
function transform(hooks) {
@@ -61,4 +63,21 @@ test('unrelated commands do not touch the flag', async () => {
assert.equal(fs.existsSync(statePath), false);
});
test('parseCommandFile reads frontmatter description + body, LF and CRLF', () => {
const lf = path.join(tmp, 'cmd-lf.md');
fs.writeFileSync(lf, '---\ndescription: do a thing\n---\n\nthe template body\n');
assert.deepEqual(parseCommandFile(lf), { description: 'do a thing', template: 'the template body' });
// Windows checkouts (autocrlf) deliver CRLF — the parser must still match.
const crlf = path.join(tmp, 'cmd-crlf.md');
fs.writeFileSync(crlf, '---\r\ndescription: do a thing\r\n---\r\n\r\nthe template body\r\n');
assert.deepEqual(parseCommandFile(crlf), { description: 'do a thing', template: 'the template body' });
});
test('parseCommandFile returns null when there is no frontmatter', () => {
const bare = path.join(tmp, 'cmd-bare.md');
fs.writeFileSync(bare, 'no frontmatter here\n');
assert.equal(parseCommandFile(bare), null);
});
test.after(() => fs.rmSync(tmp, { recursive: true, force: true }));