mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
8d1f707557
Widget plugins can now declare capabilities.widget.slot 'hero' to render as a transparent, click-through overlay sitting on the boarding-pass bar's top edge (migration 157 persists capabilities; manifest validation server+SDK, feed exposes the slot, dashboard mounts hero frames above the pass). Sidebar stays the default slot. Replaces the trip-countdown example with Koffi, the TREK mascot: an animated suitcase with a 14-state behavior engine driven by real trip data — walking, waving, napping, trolley rolls, passport-stamp stickers, a split-flap luggage- tag countdown under 7 days, and sunglasses while the trip runs. Validated by the SDK suite like any author plugin; published as mauriceboe/trek-plugin-koffi in the registry. Migrations 156/157 follow the idempotent ALTER pattern (the reconciliation test re-runs everything from v135, so duplicate-column must stay non-fatal).
80 lines
3.7 KiB
TypeScript
80 lines
3.7 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import type BetterSqlite3 from 'better-sqlite3';
|
|
import { pluginsCodeRoot, pluginCodeDir } from '../paths';
|
|
import { parseManifest, type PluginManifest } from './manifest';
|
|
import { scanForNativeBinaries } from './native-scan';
|
|
|
|
/**
|
|
* Discover plugins placed on the /plugins volume (#plugins, M4, "install from
|
|
* disk"). Reads each subdir's trek-plugin.json and upserts a registry row as
|
|
* INACTIVE — an existing plugin keeps its status / granted permissions / config,
|
|
* so re-discovery never silently re-activates or wipes settings. A plugin whose
|
|
* manifest is invalid or that ships native binaries is skipped (recorded to its
|
|
* error log if it already existed).
|
|
*/
|
|
export function discoverPlugins(db: BetterSqlite3.Database): { discovered: string[]; skipped: string[] } {
|
|
const root = pluginsCodeRoot();
|
|
const discovered: string[] = [];
|
|
const skipped: string[] = [];
|
|
if (!fs.existsSync(root)) return { discovered, skipped };
|
|
|
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
const dir = pluginCodeDir(entry.name);
|
|
const manifestPath = path.join(dir, 'trek-plugin.json');
|
|
if (!fs.existsSync(manifestPath)) continue;
|
|
|
|
try {
|
|
const manifest = parseManifest(JSON.parse(fs.readFileSync(manifestPath, 'utf8')));
|
|
if (manifest.id !== entry.name) throw new Error(`manifest id "${manifest.id}" != directory "${entry.name}"`);
|
|
if (scanForNativeBinaries(dir).length) throw new Error('directory contains native binaries');
|
|
upsert(db, manifest);
|
|
discovered.push(manifest.id);
|
|
} catch (e) {
|
|
skipped.push(entry.name);
|
|
const msg = e instanceof Error ? e.message : 'invalid plugin';
|
|
db.prepare('INSERT INTO plugin_error_log (plugin_id, level, message) VALUES (?, ?, ?)').run(entry.name, 'error', `discovery: ${msg}`);
|
|
}
|
|
}
|
|
return { discovered, skipped };
|
|
}
|
|
|
|
function upsert(db: BetterSqlite3.Database, m: PluginManifest): void {
|
|
const existing = db.prepare('SELECT id FROM plugins WHERE id = ?').get(m.id) as { id: string } | undefined;
|
|
if (existing) {
|
|
db.prepare(
|
|
`UPDATE plugins SET name = ?, description = ?, type = ?, icon = ?, version = ?, api_version = ?,
|
|
min_trek_version = ?, permissions = ?, capabilities = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
|
).run(m.name, m.description ?? null, m.type, m.icon ?? 'Blocks', m.version, m.apiVersion, m.minTrekVersion ?? null, JSON.stringify(m.permissions), JSON.stringify(m.capabilities), m.id);
|
|
} else {
|
|
db.prepare(
|
|
`INSERT INTO plugins (id, name, description, type, icon, version, api_version, min_trek_version, permissions, capabilities, granted_permissions, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '[]', 'inactive')`,
|
|
).run(m.id, m.name, m.description ?? null, m.type, m.icon ?? 'Blocks', m.version, m.apiVersion, m.minTrekVersion ?? null, JSON.stringify(m.permissions), JSON.stringify(m.capabilities));
|
|
}
|
|
|
|
// Refresh the settings-field descriptors from the manifest.
|
|
db.prepare('DELETE FROM plugin_settings_fields WHERE plugin_id = ?').run(m.id);
|
|
const insert = db.prepare(
|
|
`INSERT INTO plugin_settings_fields (plugin_id, field_key, label, input_type, placeholder, hint, required, secret, scope, options, oauth_config, sort_order)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
);
|
|
m.settings.forEach((f, i) => {
|
|
insert.run(
|
|
m.id,
|
|
f.key,
|
|
f.label ?? f.key,
|
|
f.input_type ?? 'text',
|
|
f.placeholder ?? null,
|
|
f.hint ?? null,
|
|
f.required ? 1 : 0,
|
|
f.secret ? 1 : 0,
|
|
f.scope ?? 'instance',
|
|
f.options ? JSON.stringify(f.options) : null,
|
|
f.oauth ? JSON.stringify(f.oauth) : null,
|
|
i,
|
|
);
|
|
});
|
|
}
|