mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
c3c70bcf9d
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.
131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* create-trek-plugin <name> [--type integration|page|widget] (#plugins, M6).
|
||
* Scaffolds a working plugin: manifest, an isolated server entry using
|
||
* definePlugin, a README you must fill in, and (page/widget) a starter iframe.
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
export function scaffold(name: string, type: string, targetDir: string): void {
|
||
if (!/^[a-z][a-z0-9-]{2,39}$/.test(name)) throw new Error(`invalid plugin id "${name}" (lowercase slug, 3–40 chars)`);
|
||
if (!['integration', 'page', 'widget'].includes(type)) throw new Error(`invalid type "${type}"`);
|
||
|
||
const root = path.join(targetDir, name);
|
||
if (fs.existsSync(root)) throw new Error(`${root} already exists`);
|
||
fs.mkdirSync(path.join(root, 'server'), { recursive: true });
|
||
|
||
const manifest: Record<string, unknown> = {
|
||
id: name,
|
||
name: name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
||
version: '1.0.0',
|
||
apiVersion: 1,
|
||
author: 'Your Name',
|
||
description: 'Describe what your plugin does.',
|
||
type,
|
||
trek: '>=3.2.0 <4.0.0',
|
||
nativeModules: false,
|
||
permissions: ['db:own'],
|
||
routes: [{ method: 'GET', path: '/hello', auth: true }],
|
||
};
|
||
if (type === 'page') manifest.capabilities = { nav: { label: manifest.name, icon: 'Blocks', position: 'main' } };
|
||
if (type === 'widget') manifest.capabilities = { widget: { title: manifest.name, defaultSize: 'medium' } };
|
||
|
||
fs.writeFileSync(path.join(root, 'trek-plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
||
fs.writeFileSync(path.join(root, 'server', 'index.js'), SERVER_JS);
|
||
fs.writeFileSync(path.join(root, 'README.md'), readme(name));
|
||
if (type !== 'integration') {
|
||
fs.mkdirSync(path.join(root, 'client'), { recursive: true });
|
||
fs.writeFileSync(path.join(root, 'client', 'index.html'), CLIENT_HTML);
|
||
}
|
||
}
|
||
|
||
const SERVER_JS = `// Built plugin entry — runs in an isolated child process.
|
||
const { definePlugin } = require('trek-plugin-sdk');
|
||
|
||
module.exports = definePlugin({
|
||
async onLoad(ctx) {
|
||
await ctx.db.migrate('001_init', 'CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)');
|
||
ctx.log.info('plugin loaded');
|
||
},
|
||
routes: [
|
||
{
|
||
method: 'GET', path: '/hello', auth: true,
|
||
async handler(req, ctx) {
|
||
return { status: 200, headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({ hello: req.user && req.user.username }) };
|
||
},
|
||
},
|
||
],
|
||
});
|
||
`;
|
||
|
||
const CLIENT_HTML = `<!doctype html>
|
||
<html><head><meta charset="utf-8" /><title>Plugin</title></head>
|
||
<body>
|
||
<h1>Hello from your plugin</h1>
|
||
<script>
|
||
// The frame is sandboxed (opaque origin) — talk to TREK only via postMessage.
|
||
window.parent.postMessage({ type: 'trek:ready' }, '*');
|
||
window.addEventListener('message', (e) => {
|
||
if (e.data && e.data.type === 'trek:context') {
|
||
document.body.dataset.theme = e.data.theme;
|
||
}
|
||
});
|
||
</script>
|
||
</body></html>
|
||
`;
|
||
|
||
function readme(name: string): string {
|
||
return `# ${name}
|
||
|
||
> One sentence: what this plugin does.
|
||
|
||

|
||
|
||
## What it does
|
||
|
||
Describe the feature this plugin adds to TREK.
|
||
|
||
## Screenshots
|
||
|
||
Show it in context. Commit a \`docs/screenshot.png\` — it's what the store card
|
||
shows. A 16:9 image (e.g. 1600×900) with your plugin centred and some margin
|
||
looks best (the card crops the edges).
|
||
|
||
## Permissions
|
||
|
||
| Permission | Why |
|
||
|---|---|
|
||
| \`db:own\` | store the plugin's own data |
|
||
|
||
## Setup
|
||
|
||
How to configure it.
|
||
|
||
## License
|
||
|
||
Your plugin is your own code — license it however you like; TREK does not impose
|
||
one. Replace this line with your license (for example, MIT).
|
||
`;
|
||
}
|
||
|
||
// CLI entry
|
||
if (process.argv[1] && process.argv[1].endsWith('create.js')) {
|
||
const args = process.argv.slice(2);
|
||
const name = args.find((a: string) => !a.startsWith('-'));
|
||
const typeIdx = args.indexOf('--type');
|
||
const type = typeIdx >= 0 ? args[typeIdx + 1] : 'integration';
|
||
if (!name) {
|
||
console.error('usage: create-trek-plugin <name> [--type integration|page|widget]');
|
||
process.exit(2);
|
||
}
|
||
try {
|
||
scaffold(name, type, process.cwd());
|
||
console.log(`Created ${name}/ — fill in the README, build server/index.js, then \`npx trek-plugin validate ${name}\`.`);
|
||
} catch (e) {
|
||
console.error(e instanceof Error ? e.message : e);
|
||
process.exit(1);
|
||
}
|
||
}
|