From 9e4964ea4ffec66b40df228f984edab5c2107cf2 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 4 Jul 2026 17:26:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(plugin-sdk):=20one-command=20`publish`=20(?= =?UTF-8?q?pack=20=E2=86=92=20release=20=E2=86=92=20preflight=20=E2=86=92?= =?UTF-8?q?=20PR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapses the release into a single command: pack the artifact, tag + create the GitHub release, run the registry CI checks locally (preflight), and open the registry PR — stopping before it submits if preflight would fail, so a broken entry never becomes a doomed PR. `--sign` signs it; `--no-preflight` skips the gate. The individual pack/release/preflight/submit commands still exist. README + the Development/Publishing/Permissions wikis lead with `publish` now. --- plugin-sdk/README.md | 37 +++++++--------- plugin-sdk/package.json | 2 +- plugin-sdk/src/cli/publish.ts | 70 +++++++++++++++++++++++++++++++ plugin-sdk/src/cli/trek-plugin.ts | 18 ++++++-- wiki/Plugin-Permissions.md | 1 + wiki/Plugin-Publishing.md | 24 ++++++----- 6 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 plugin-sdk/src/cli/publish.ts diff --git a/plugin-sdk/README.md b/plugin-sdk/README.md index 0bd7684f1..52897fc20 100644 --- a/plugin-sdk/README.md +++ b/plugin-sdk/README.md @@ -59,33 +59,26 @@ const { ctx, broadcasts } = createMockHost({ // degrades gracefully when a permission is missing. ``` -## Publish +## Publish — one command -The SDK does the fiddly parts (zipping, hashing, sizing, writing the registry -entry, opening the PR) so you don't compute anything by hand. The short path: +Commit and push your plugin to its public GitHub repo, then: ```bash -git tag v1.0.0 && git push --tags -npx trek-plugin-sdk release --repo you/repo --tag v1.0.0 # pack -> GitHub release -> entry -npx trek-plugin-sdk submit --repo you/repo --tag v1.0.0 # opens the registry PR for you +npx trek-plugin-sdk publish --repo you/repo --tag v1.0.0 ``` -`submit` forks [TREK-Plugins](https://github.com/mauriceboe/TREK-Plugins), -branches off the current main, writes (or merges into) `registry/plugins/.json`, -and opens the PR — the last manual step is gone. Prefer to paste it yourself? -`entry` just prints the JSON. +`publish` does the whole release in one go: **pack** → **tag + GitHub release** +→ **preflight** (runs the registry CI checks locally) → **open the registry PR**. +If preflight finds a problem it stops *before* submitting, so a broken entry +never becomes a doomed PR. It prints the PR URL at the end. -**Check before you submit** — run the exact registry CI checks locally so you -don't round-trip through review: +**Updating** a listed plugin: bump `version` in the manifest, commit, and run +`publish` again with the new tag — it detects the existing entry and prepends the +new version, newest-first. -```bash -npx trek-plugin-sdk preflight --repo you/repo --tag v1.0.0 -# verifies the tag→commit, manifest parity, artifact sha256/size, native scan, -# and the README quality gate — over the network, against your pushed release. -``` - -**Updating** a listed plugin: bump `version`, tag, and re-run `submit` — it -detects the existing entry and prepends the new version, newest-first. +Prefer to drive the steps yourself? They still exist individually — `pack`, +`release` (pack → GitHub release → entry), `preflight`, `submit` (opens the PR), +and `entry` (just prints the JSON). ### Sign your releases (optional, recommended) @@ -94,8 +87,7 @@ Give your plugin a stable identity. TREK pins your key on first install ```bash npx trek-plugin-sdk keygen # once — writes ~/.trek-plugin/signing.key -npx trek-plugin-sdk release --repo you/repo --tag v1.1.0 --sign -npx trek-plugin-sdk submit --repo you/repo --tag v1.1.0 --sign +npx trek-plugin-sdk publish --repo you/repo --tag v1.1.0 --sign ``` Signing is dependency-free Ed25519 over the artifact bytes. **Back up the key** — @@ -123,5 +115,6 @@ bin if you install the package): - `preflight --repo o/n --tag vX` (or `--entry f`) — run the registry CI checks locally, over the network. - `submit --repo o/n --tag vX [--sign [key]] [--draft]` — open the registry PR for you. - `release [dir] --repo o/n --tag vX [--sign [key]] [--merge f]` — pack → GitHub release → entry, in one go. +- `publish [dir] --repo o/n --tag vX [--sign [key]] [--no-preflight]` — **the lot**: pack → tag + release → preflight → open the PR. The SDK tooling in this repo is MIT. Your plugin is your own code under your own license. diff --git a/plugin-sdk/package.json b/plugin-sdk/package.json index 5af7370c2..773ed7c3d 100644 --- a/plugin-sdk/package.json +++ b/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "trek-plugin-sdk", - "version": "1.1.0", + "version": "1.2.0", "description": "SDK for building TREK plugins: types, definePlugin, a mock host, manifest validation, and the create-trek-plugin / trek-plugin CLIs.", "type": "module", "license": "MIT", diff --git a/plugin-sdk/src/cli/publish.ts b/plugin-sdk/src/cli/publish.ts new file mode 100644 index 000000000..5efb4f2d7 --- /dev/null +++ b/plugin-sdk/src/cli/publish.ts @@ -0,0 +1,70 @@ +/** + * trek-plugin publish — the whole release in one command: pack → tag + GitHub + * release → preflight (the registry CI checks, locally) → open the registry PR. + * If preflight fails it stops before submitting, so a broken entry never becomes + * a doomed PR. This is the short path; the individual commands still exist for + * when you want a step by hand. + * + * Requires `git` + `gh` (authenticated), same as `release`/`submit`. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { packPluginDir } from './pack.js'; +import { buildEntry } from './entry.js'; +import { preflight } from './preflight.js'; +import { submitEntry } from './submit.js'; + +function git(dir: string, args: string[], quiet = true): string { + return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit' }).toString().trim(); +} +function tagExists(dir: string, tag: string): boolean { + try { git(dir, ['rev-parse', `${tag}^{commit}`]); return true; } catch { return false; } +} + +export async function publishPlugin(opts: { + dir: string; repo: string; tag: string; now: string; + signKeyPath?: string; registry?: string; draft?: boolean; notes?: string; skipPreflight?: boolean; +}): Promise<{ prUrl: string }> { + const dir = path.resolve(opts.dir); + const step = (n: number, msg: string) => console.error(`[${n}/4] ${msg}`); + + // 1. Pack + step(1, 'Packing the artifact…'); + const zip = path.join(dir, 'plugin.zip'); + const packed = packPluginDir(dir, zip); + console.error(` ✓ ${packed.files.length} files, ${packed.size} bytes`); + + // 2. Tag (if needed) + push + GitHub release with the artifact attached + step(2, `Tagging ${opts.tag} + creating the GitHub release…`); + if (!tagExists(dir, opts.tag)) git(dir, ['tag', opts.tag]); + try { git(dir, ['push', 'origin', opts.tag]); } catch { + throw new Error(`could not push tag ${opts.tag} — is "origin" your plugin's GitHub repo and are you authenticated? (git push origin ${opts.tag})`); + } + try { + execFileSync('gh', ['release', 'create', opts.tag, packed.artifact, '--repo', opts.repo, '--title', opts.tag, '--notes', opts.notes || `Release ${opts.tag}`], { stdio: 'pipe' }); + } catch { + // Release already exists — (re)upload the packed artifact so its bytes match the pin. + execFileSync('gh', ['release', 'upload', opts.tag, packed.artifact, '--repo', opts.repo, '--clobber'], { stdio: 'pipe' }); + } + console.error(` ✓ release ${opts.tag} on ${opts.repo}`); + + // 3. Build the entry, then run the registry CI checks locally + const entry = buildEntry({ dir, repo: opts.repo, tag: opts.tag, zipPath: packed.artifact, signKeyPath: opts.signKeyPath, now: opts.now }); + if (opts.skipPreflight) { + console.error('[3/4] Preflight skipped (--no-preflight).'); + } else { + step(3, 'Preflight — running the registry CI checks…'); + const rep = await preflight(entry); + for (const f of rep.failures) console.error(' ✗ ' + f); + if (!rep.ok) throw new Error(`preflight found ${rep.failures.length} problem(s) — fix these and re-run (nothing was submitted). Did you push your code to ${opts.repo} before publishing?`); + console.error(` ✓ all ${rep.passed.length} checks passed`); + } + + // 4. Open the registry PR + step(4, 'Opening the registry PR…'); + const { prUrl } = submitEntry(entry, { registry: opts.registry, draft: opts.draft }); + console.error(' ✓ done'); + fs.rmSync(zip, { force: true }); + return { prUrl }; +} diff --git a/plugin-sdk/src/cli/trek-plugin.ts b/plugin-sdk/src/cli/trek-plugin.ts index 81aecf203..bacf0d31f 100644 --- a/plugin-sdk/src/cli/trek-plugin.ts +++ b/plugin-sdk/src/cli/trek-plugin.ts @@ -15,9 +15,11 @@ * [--sign [key]] [--registry o/n] [--draft] * release [dir] --repo o/n --tag vX pack -> gh release -> print entry * [--sign [key]] [--merge entry.json] + * publish [dir] --repo o/n --tag vX the lot: pack -> tag+release -> + * [--sign [key]] [--no-preflight] preflight -> open the registry PR * - * The goal: create -> dev -> release/submit, and never hand-compute sha256/size/ - * commitSha or hand-write the registry JSON. + * The goal: create -> dev -> publish, and never hand-compute sha256/size/commitSha + * or hand-write the registry JSON. */ import fs from 'node:fs'; import path from 'node:path'; @@ -29,6 +31,7 @@ import { scaffold, interactiveScaffold } from './create.js'; import { runDev } from './dev.js'; import { preflight } from './preflight.js'; import { submitEntry } from './submit.js'; +import { publishPlugin } from './publish.js'; import { generateKeypair, loadPrivateKey, signArtifact, publicKeyBase64, defaultKeyPath } from './sign.js'; const [cmd, ...args] = process.argv.slice(2); @@ -131,6 +134,15 @@ async function main(): Promise { for (const f of rep.failures) console.error(' ✗ ' + f); if (!rep.ok) { console.error(`\n${rep.failures.length} check(s) would fail CI — fix these before submitting.`); process.exit(1); } console.error('\n✓ all checks passed — this entry should sail through CI.'); + } else if (cmd === 'publish') { + if (!flags.repo || !flags.tag) fail('publish needs --repo and --tag '); + const { prUrl } = await publishPlugin({ + dir: pos[0] || '.', repo: flags.repo, tag: flags.tag, + signKeyPath: signKey(), registry: flags.registry, draft: !!flags.draft, + notes: flags.notes, skipPreflight: !!flags['no-preflight'], now: new Date().toISOString(), + }); + console.error('\n✓ published — registry PR:'); + console.log(prUrl); } else if (cmd === 'submit') { if (!flags.repo || !flags.tag) fail('submit needs --repo and --tag '); const entry = buildEntry({ @@ -153,7 +165,7 @@ async function main(): Promise { console.error('\nRegistry entry (add as registry/plugins/' + entry.id + '.json in a TREK-Plugins PR, or run `trek-plugin submit`):\n'); process.stdout.write(JSON.stringify(entry, null, 2) + '\n'); } else { - console.error('usage: trek-plugin [...]'); + console.error('usage: trek-plugin [...]'); process.exit(2); } } diff --git a/wiki/Plugin-Permissions.md b/wiki/Plugin-Permissions.md index 354e49356..1f3fa48c8 100644 --- a/wiki/Plugin-Permissions.md +++ b/wiki/Plugin-Permissions.md @@ -82,6 +82,7 @@ flow is in [[Publishing a Plugin|Plugin-Publishing]]. | `trek-plugin validate [dir]` | Manifest + layout checks: parses the manifest with the same rules as install, requires a `README.md` (warns if it has no screenshot or still holds template placeholders) and a built `server/index.js`, and warns if the directory name ≠ the plugin id. This is a **subset** of registry CI — CI additionally verifies the release tag/commit, the artifact's sha256, and the README over the network. A local pass predicts a CI pass. | | `trek-plugin preflight --repo --tag ` | Runs the **full** registry CI checks locally over the network (tag→commit, manifest parity, artifact sha256/size, native scan, README quality gate) against your pushed release — so you catch a CI failure before opening the PR. | | `trek-plugin submit --repo --tag ` | Opens the registry PR for you: forks TREK-Plugins, branches off current main, writes/merges `registry/plugins/.json`, pushes, and creates the PR. Requires `gh`. | +| `trek-plugin publish --repo --tag ` | **The one-command release**: pack → tag + GitHub release → preflight → open the registry PR. Stops before submitting if preflight fails. Add `--sign` to sign it. Requires `git` + `gh`. | | `trek-plugin keygen` / `sign` | `keygen` creates an Ed25519 signing key; `sign` (or `--sign` on `entry`/`release`/`submit`) signs the artifact and fills `authorPublicKey` + `signature` so TREK pins your identity (TOFU). | | `trek-plugin pack [dir] [--out plugin.zip] [--json]` | Validates, then builds `plugin.zip` in the installer's exact layout (`trek-plugin.json`, `README.md`, `LICENSE`, `package.json` at the root; `server/` and `client/` recursed) and prints its **sha256 + byte size**. Skips `node_modules`, `.git`, `.ts` and `.map` files, and **refuses native binaries** (`.node`, `binding.gyp`, `prebuilds/`) and over-size archives, same as the installer. **`docs/` is intentionally NOT shipped** — the store fetches your screenshot from `docs/screenshot.png` in the repo. | | `trek-plugin entry --repo --tag [--zip plugin.zip] [--merge entry.json] [--out file]` | Emits the ready-to-PR registry entry: `commitSha` (resolved from the tag), `downloadUrl`, `sha256` + `size` (from the packed zip), and `minTrekVersion` (derived from the manifest's `trek` range, e.g. `>=3.2.0 <4.0.0` → `3.2.0`). `--merge` prepends this version onto an existing `registry/plugins/.json` for an update, keeping versions newest-first. | diff --git a/wiki/Plugin-Publishing.md b/wiki/Plugin-Publishing.md index 71bde371d..7c35f54e2 100644 --- a/wiki/Plugin-Publishing.md +++ b/wiki/Plugin-Publishing.md @@ -8,21 +8,23 @@ attach a built `plugin.zip` to a release, and list it with a pull request. The `trek-plugin` CLI (shipped in the `trek-plugin-sdk` package) does almost all the mechanical work — you rarely hand-type a hash, size, commit, or JSON field. -## The short version +## The short version — one command + +Commit and push your plugin to its public GitHub repo, then: ```bash -git tag v1.0.0 && git push --tags -npx trek-plugin-sdk release --repo you/trek-plugin-flight-tracker --tag v1.0.0 # pack → GitHub release → entry -npx trek-plugin-sdk preflight --repo you/trek-plugin-flight-tracker --tag v1.0.0 # the CI checks, locally -npx trek-plugin-sdk submit --repo you/trek-plugin-flight-tracker --tag v1.0.0 # opens the registry PR +npx trek-plugin-sdk publish --repo you/trek-plugin-flight-tracker --tag v1.0.0 ``` -`release` builds the artifact, creates the GitHub release, and prints the -registry entry. `preflight` runs the exact registry CI checks against your -pushed release so you don't round-trip through review. `submit` forks -**TREK-Plugins**, writes `registry/plugins/.json`, and opens the PR for you. -Prefer to do the last step by hand? `entry --out registry/plugins/.json` -prints the file and you open the PR yourself. +`publish` runs the whole release: **pack** → **tag + GitHub release** → +**preflight** (the registry CI checks, locally) → **open the registry PR**. If +preflight finds a problem it stops *before* submitting, so a broken entry never +becomes a doomed PR. It prints the PR URL at the end. Sign it with `--sign`. + +The individual steps below still exist for when you want one by hand — `release` +(pack → GitHub release → entry), `preflight`, and `submit` (opens the PR) — or +`entry --out registry/plugins/.json` to write the file and open the PR +yourself. ## 1. Host and build your plugin