mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(plugin-sdk): dev server, preflight, auto-PR submit, signing, wizard
Round out the author experience so the loop is create -> dev -> release/submit without hand-work or a round-trip through registry review. - `dev`: run a plugin locally with a real request loop and hot reload — no full TREK. Injects a ctx that enforces the manifest's granted permissions (an ungranted call throws, so you catch a missing grant), backs db:own with a real SQLite file (node:sqlite), serves routes under /api and page/widget UI at /ui, and reloads on save. Dependency-free (node:http + built-ins). - `preflight`: run the registry CI checks locally over the network (tag->commit, manifest parity, artifact sha256/size, native scan, README quality gate) so a green run predicts a green CI. - `submit`: fork TREK-Plugins, branch off current main, write/merge the entry, push, and open the PR — the last manual publishing step, automated. - `keygen`/`sign` + `--sign` on entry/release/submit: dependency-free Ed25519 author signatures over the artifact bytes, verified 1:1 against the server's TOFU check. Fills authorPublicKey + signature and guards against a key change. - `create` gains an interactive wizard (id/type/author/permissions) and flags. - README + Development/Publishing/Permissions wikis document the new flow. 24 tests pass (sign round-trips through a server-shaped verifier; zip reader; scaffold options; entry signing + key-change guard).
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* trek-plugin submit — open the TREK-Plugins registry PR for you. Forks the
|
||||
* registry (once), branches off the current upstream main, writes (or merges
|
||||
* into) registry/plugins/<id>.json, commits, pushes, and opens the PR — so the
|
||||
* last manual step of publishing (fork, paste JSON, open PR by hand) is gone.
|
||||
*
|
||||
* Requires `gh` (authenticated) and `git`, same as `release`.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const DEFAULT_REGISTRY = 'mauriceboe/TREK-Plugins';
|
||||
|
||||
interface EntryLike {
|
||||
id: string; name?: string; authorPublicKey?: string;
|
||||
versions: { version: string }[];
|
||||
}
|
||||
|
||||
function git(cwd: string, ...a: string[]): string {
|
||||
return execFileSync('git', ['-C', cwd, ...a], { encoding: 'utf8' }).trim();
|
||||
}
|
||||
function gh(...a: string[]): string {
|
||||
return execFileSync('gh', a, { encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
/** Merge a freshly-built single-version entry onto an existing registry file (update case). */
|
||||
function mergeOnto(existing: EntryLike, fresh: EntryLike): EntryLike {
|
||||
const v = fresh.versions[0];
|
||||
if (existing.authorPublicKey && fresh.authorPublicKey && existing.authorPublicKey !== fresh.authorPublicKey) {
|
||||
throw new Error('the signing key differs from the one already published for this plugin — TREK would reject the update. Use the original key.');
|
||||
}
|
||||
if (existing.authorPublicKey && !fresh.authorPublicKey) {
|
||||
throw new Error('this plugin was published signed — sign the update too (pass --sign) or TREK will refuse it.');
|
||||
}
|
||||
const versions = [v, ...existing.versions.filter((x) => x.version !== v.version)];
|
||||
const merged: EntryLike = { ...existing, ...fresh, versions };
|
||||
merged.authorPublicKey = fresh.authorPublicKey ?? existing.authorPublicKey;
|
||||
if (merged.authorPublicKey === undefined) delete merged.authorPublicKey;
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function submitEntry(entry: EntryLike, opts: { registry?: string; branch?: string; draft?: boolean; keep?: boolean } = {}): { prUrl: string } {
|
||||
const registry = opts.registry || DEFAULT_REGISTRY;
|
||||
const name = registry.split('/')[1];
|
||||
const login = gh('api', 'user', '--jq', '.login');
|
||||
const branch = opts.branch || `plugin-${entry.id}-${entry.versions[0].version}`;
|
||||
|
||||
// Ensure the fork exists (idempotent — prints "already exists" if it does).
|
||||
try { gh('repo', 'fork', registry, '--clone=false'); } catch { /* already forked */ }
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-submit-'));
|
||||
try {
|
||||
// Clone the fork (pushable via gh auth), then base a branch on the CURRENT upstream main.
|
||||
let cloned = false;
|
||||
for (let i = 0; i < 3 && !cloned; i++) {
|
||||
try { gh('repo', 'clone', `${login}/${name}`, tmp, '--', '--depth=1'); cloned = true; }
|
||||
catch { /* fork may not be ready yet; retry */ }
|
||||
}
|
||||
if (!cloned) throw new Error(`could not clone your fork ${login}/${name} (is the fork ready on GitHub?)`);
|
||||
|
||||
git(tmp, 'remote', 'add', 'upstream', `https://github.com/${registry}.git`);
|
||||
git(tmp, 'fetch', '--depth=1', 'upstream', 'main');
|
||||
git(tmp, 'checkout', '-B', branch, 'upstream/main');
|
||||
|
||||
const rel = path.join('registry', 'plugins', `${entry.id}.json`);
|
||||
const abs = path.join(tmp, rel);
|
||||
let toWrite = entry;
|
||||
let action = 'add';
|
||||
if (fs.existsSync(abs)) {
|
||||
const existing = JSON.parse(fs.readFileSync(abs, 'utf8')) as EntryLike;
|
||||
toWrite = mergeOnto(existing, entry);
|
||||
action = 'update';
|
||||
}
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, JSON.stringify(toWrite, null, 2) + '\n');
|
||||
|
||||
const title = action === 'add'
|
||||
? `Add ${entry.name || entry.id} (${entry.versions[0].version})`
|
||||
: `Update ${entry.name || entry.id} to ${entry.versions[0].version}`;
|
||||
git(tmp, 'add', rel);
|
||||
git(tmp, 'commit', '-m', title);
|
||||
git(tmp, 'push', '--force-with-lease', 'origin', branch);
|
||||
|
||||
const body = [
|
||||
`${action === 'add' ? 'New plugin' : 'Plugin update'}: **${entry.name || entry.id}** \`${entry.id}\` ${entry.versions[0].version}.`,
|
||||
'',
|
||||
'Generated with `trek-plugin submit`. CI validates the tag, artifact hash, manifest parity and README.',
|
||||
].join('\n');
|
||||
const args = ['pr', 'create', '--repo', registry, '--head', `${login}:${branch}`, '--title', title, '--body', body];
|
||||
if (opts.draft) args.push('--draft');
|
||||
const prUrl = gh(...args);
|
||||
return { prUrl };
|
||||
} finally {
|
||||
if (!opts.keep) fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user