plugins page becomes a marketplace: catalogue, clone-on-install
The /plugins route showed only what was already on disk, so a plugin nobody had copied in by hand did not exist. It now shows a third list — what a remote catalogue offers — and installing one fetches its source. - plugins/marketplace.ts — a stand-in for the remote store, a JSON array in process. One entry: music, pointing at gitea.officer.dev/plugins/music. It is deliberately a module and not a fetch, so the shape of a catalogue entry gets settled before anything depends on a server answering. - plugins/fetch.ts — git clone --depth 1 into plugins/<appName>/, INSIDE the platform repo because bun resolves the workspace links by walking up and a plugin cloned beside it cannot import anything the platform provides (measured, not assumed). https only, no credentials, GIT_TERMINAL_PROMPT=0 so a repo that turned private fails instead of hanging. A failed clone removes the directory: a half-written one reads as a broken plugin rather than an absent one, which is the worse of the two. - install.ts fetches first when the directory is missing, then RE-DISCOVERS rather than assuming what it cloned. Everything after is the existing path, including the frontend rebuild — so a cloned plugin's panels reach the SPA without a restart. - GET /api/plugins returns `available`, catalogue minus what is on disk. Each entry gets the same live PATH probe a local plugin gets, so music's ffmpeg and ffprobe are answered for this machine BEFORE the clone rather than after. - The detail panel is a separate component for a catalogue entry, not a mode of the installed one: there is no sidecar status, no enabled state and no route to open, and what there IS — the source URL — the installed panel has no reason to show. The streaming log is shared, because the marketplace install is the case that needs it most: it clones before it does anything, and a slow clone behind a blank panel looks like a hang. The catalogue's icon is an absolute URL to the store's copy, which is the [open] question the previous commit left: assets only publish at install, so a plugin you have never installed had no artwork to draw. Not done: no update verb, no signature, no private repos, and removePluginSource has no caller — deleting source drops tables on the next push, and that wants a verb that says so. tsgo clean. 767 pass / 6 fail, all 6 identical on HEAD without this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { isSuperAdmin } from '../../super-admin';
|
||||
import { mountPrefix } from '../../plugins/manifest';
|
||||
import { snapshotPlugins } from '../../plugins/mount';
|
||||
import { manualInstallHint, reportDependencies } from '../../plugins/os-deps';
|
||||
import { fetchCatalogue } from '../../plugins/marketplace';
|
||||
import { iconUrl } from '../../app-store/assets';
|
||||
import {
|
||||
installPlugin,
|
||||
@@ -59,7 +60,48 @@ pluginsRouter.get('/', async (ctx) => {
|
||||
manualHint: manualInstallHint(report.manager, report.packagesToInstall),
|
||||
};
|
||||
});
|
||||
// What the marketplace offers that this machine does not have.
|
||||
//
|
||||
// Merged here rather than in `snapshotPlugins`, which answers "what is on disk joined to what is
|
||||
// installed" and should keep answering exactly that — the catalogue is a third, remote source and
|
||||
// giving discovery a network dependency would make the plugins page fail when the store is down.
|
||||
//
|
||||
// A catalogue entry whose plugin is already on disk is DROPPED: the directory is the truth once it
|
||||
// exists, and showing both would offer to install something already installed.
|
||||
const onDisk = new Set(states.map((s) => s.plugin.appName));
|
||||
const catalogue = (await fetchCatalogue().catch(() => [])).filter((e) => !onDisk.has(e.appName));
|
||||
|
||||
return ctx.json({
|
||||
available: catalogue.map((e) => ({
|
||||
appName: e.appName,
|
||||
prefix: e.publisher === 'officerdev' ? `/${e.appName}` : `/p/${e.publisher}/${e.appName}`,
|
||||
label: e.label,
|
||||
summary: e.summary,
|
||||
image: e.iconUrl,
|
||||
tile: e.tile ?? 'badge',
|
||||
color: e.color,
|
||||
publisher: e.publisher,
|
||||
version: e.version,
|
||||
platform: e.platform,
|
||||
permissions: e.permissions,
|
||||
has: e.has,
|
||||
source: e.source,
|
||||
// The same live PATH probe a local plugin gets, so ffmpeg/ffprobe are visible BEFORE the clone
|
||||
// rather than after. `reportDependencies` takes a DiscoveredPlugin, so the entry is adapted to
|
||||
// that shape — the only field it reads is `manifest.osDependencies`.
|
||||
dependencies: (() => {
|
||||
const r = reportDependencies({ manifest: { osDependencies: e.osDependencies } } as never);
|
||||
return {
|
||||
manager: r.manager,
|
||||
items: r.dependencies,
|
||||
satisfied: r.missing.length === 0,
|
||||
canInstall: r.canInstall,
|
||||
blockedReason: r.blockedReason,
|
||||
packagesToInstall: r.packagesToInstall,
|
||||
manualHint: manualInstallHint(r.manager, r.packagesToInstall),
|
||||
};
|
||||
})(),
|
||||
})),
|
||||
plugins: states.map(({ plugin, install, outdated }, i) => ({
|
||||
appName: plugin.appName,
|
||||
prefix: mountPrefix(plugin),
|
||||
|
||||
Reference in New Issue
Block a user