feat(plugin-sdk): one-command publishing — pack, entry, release

Publishing a plugin meant hand-building the zip, running shasum + stat,
resolving the tag's commit, and hand-writing the whole registry entry.
The SDK does all of it now:

- `trek-plugin pack` builds plugin.zip in the exact layout the installer
  reads (own tiny zip writer, so the SDK stays dependency-free and the
  format can't drift from the reader), enforces the same native-binary and
  size rules, and prints the sha256 + size. docs/ is left out — the store
  fetches the screenshot from the repo, so it doesn't belong in the install
  artifact (Koffi's went from 943 KB to 15 KB).
- `trek-plugin entry` emits the ready-to-PR registry entry from the manifest
  + the packed zip + the git tag: commitSha (deref'd), downloadUrl, sha256,
  size, and minTrekVersion derived from the manifest's trek range. `--merge`
  prepends a new version onto an existing entry for updates.
- `trek-plugin release` chains pack → gh release → entry.

Also: the scaffold now points the README at docs/screenshot.png (the path
the store actually fetches, was screenshot-1.png) with a size hint, and
stops hard-coding an MIT license — a plugin is the author's own code under
their own license. Round-tripped against the real server extractor; 18 tests.
This commit is contained in:
Maurice
2026-07-04 13:57:45 +02:00
committed by Maurice
parent bc56c2dcc1
commit c3c70bcf9d
7 changed files with 491 additions and 20 deletions
+86 -13
View File
@@ -1,19 +1,92 @@
#!/usr/bin/env node
/** `trek-plugin <command>` dispatcher (#plugins, M6). Commands: validate. */
/**
* `trek-plugin <command>` — the plugin author CLI (#plugins).
*
* validate [dir] check the manifest + layout
* pack [dir] [--out plugin.zip] [--json] build plugin.zip, print sha256 + size
* entry --repo o/n --tag vX [--zip z] print the ready-to-PR registry entry
* [--merge entry.json] [--out f]
* release [dir] --repo o/n --tag vX pack -> gh release -> print entry
*
* The goal: an author runs `pack` then `entry` (or a single `release`) and never
* hand-computes sha256/size/commitSha or hand-writes the registry JSON.
*/
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { validatePluginDir } from './validate.js';
import { packPluginDir } from './pack.js';
import { buildEntry } from './entry.js';
const [cmd, ...rest] = process.argv.slice(2);
const [cmd, ...args] = process.argv.slice(2);
if (cmd === 'validate') {
const r = validatePluginDir(rest[0] || '.');
for (const w of r.warnings) console.warn('warning: ' + w);
if (r.ok) {
console.log('✓ plugin is valid');
} else {
for (const e of r.errors) console.error('error: ' + e);
process.exit(1);
function parse(a: string[]): { flags: Record<string, string>; pos: string[] } {
const flags: Record<string, string> = {};
const pos: string[] = [];
for (let i = 0; i < a.length; i++) {
const t = a[i];
if (t.startsWith('--')) {
const next = a[i + 1];
flags[t.slice(2)] = next !== undefined && !next.startsWith('--') ? (i++, next) : 'true';
} else pos.push(t);
}
} else {
console.error('usage: trek-plugin validate [dir]');
process.exit(2);
return { flags, pos };
}
function fail(msg: string): never {
console.error('error: ' + msg);
process.exit(1);
}
const { flags, pos } = parse(args);
try {
if (cmd === 'validate') {
const r = validatePluginDir(pos[0] || '.');
for (const w of r.warnings) console.warn('warning: ' + w);
if (!r.ok) { for (const e of r.errors) console.error('error: ' + e); process.exit(1); }
console.log('✓ plugin is valid');
} else if (cmd === 'pack') {
const r = packPluginDir(pos[0] || '.', flags.out || 'plugin.zip');
if (flags.json) {
console.log(JSON.stringify(r, null, 2));
} else {
console.log(`Packed ${r.files.length} files -> ${path.relative(process.cwd(), r.artifact) || r.artifact}`);
for (const f of r.files) console.log(' ' + f);
console.log(`\nsha256: ${r.sha256}\nsize: ${r.size}`);
console.log('\nUpload this plugin.zip to your release, then run `trek-plugin entry` to generate the registry entry.');
}
} else if (cmd === 'entry') {
if (!flags.repo || !flags.tag) fail('entry needs --repo <owner/name> and --tag <vX.Y.Z>');
const entry = buildEntry({
dir: flags.dir || pos[0] || '.',
repo: flags.repo, tag: flags.tag,
zipPath: flags.zip || 'plugin.zip',
commit: flags.commit, asset: flags.asset, mergePath: flags.merge,
now: new Date().toISOString(),
});
const json = JSON.stringify(entry, null, 2) + '\n';
if (flags.out) {
fs.writeFileSync(flags.out, json);
console.error(`Wrote ${flags.out} — add it as registry/plugins/${entry.id}.json in a TREK-Plugins PR.`);
} else {
process.stdout.write(json);
}
} else if (cmd === 'release') {
if (!flags.repo || !flags.tag) fail('release needs --repo <owner/name> and --tag <vX.Y.Z>');
const dir = pos[0] || '.';
const zip = path.resolve(dir, flags.out || 'plugin.zip');
const packed = packPluginDir(dir, zip);
console.error(`Packed ${packed.files.length} files (${packed.size} bytes).`);
console.error(`Creating GitHub release ${flags.tag} on ${flags.repo}…`);
execFileSync('gh', ['release', 'create', flags.tag, packed.artifact, '--repo', flags.repo, '--title', flags.tag, '--notes', flags.notes || `Release ${flags.tag}`], { stdio: 'inherit' });
const entry = buildEntry({ dir, repo: flags.repo, tag: flags.tag, zipPath: packed.artifact, commit: flags.commit, mergePath: flags.merge, now: new Date().toISOString() });
console.error('\nRegistry entry (add as registry/plugins/' + entry.id + '.json in a TREK-Plugins PR):\n');
process.stdout.write(JSON.stringify(entry, null, 2) + '\n');
} else {
console.error('usage: trek-plugin <validate|pack|entry|release> [...]');
process.exit(2);
}
} catch (e) {
fail(e instanceof Error ? e.message : String(e));
}