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:
@@ -3,6 +3,7 @@ import { PLATFORM_DIR } from '../data-path';
|
||||
import { deleteProcess, processStatus, startProcess, stopProcess } from '../app-store/pm2';
|
||||
import { addPluginToEcosystem, pluginProcessName, removePluginFromEcosystem } from './ecosystem';
|
||||
import { discoverPlugins } from './discover';
|
||||
import { fetchPlugin } from './fetch';
|
||||
import { publishAssets, unpublishAssets } from '../app-store/assets';
|
||||
import { refreshPluginMounts, snapshotPlugins } from './mount';
|
||||
import { generatePluginSchemas, pushSchema } from './schema';
|
||||
@@ -95,10 +96,35 @@ async function findPlugin(appName: string): Promise<DiscoveredPlugin | null> {
|
||||
* switched off must not switch it back on.
|
||||
*/
|
||||
export async function installPlugin(appName: string, onStep?: OnStep): Promise<PluginActionResult> {
|
||||
const plugin = await findPlugin(appName);
|
||||
if (!plugin) return { ok: false, appName, steps: [], error: `No plugin directory named "${appName}"` };
|
||||
|
||||
const steps: string[] = [];
|
||||
|
||||
// FETCH FIRST, when the plugin is not here yet.
|
||||
//
|
||||
// Until 2026-08-15 every plugin shipped inside the platform's repository, so "installed" and "present"
|
||||
// were nearly the same thing and install could assume a directory. Music moving to its own repository
|
||||
// ended that: it is real, installable, and absent. So the first question is no longer "is it enabled"
|
||||
// but "is it even on this machine".
|
||||
//
|
||||
// Cloning before anything else keeps the same property every other step here has — it changes nothing
|
||||
// that needs undoing if it fails. A failed clone removes its own directory and the machine is exactly
|
||||
// as it was.
|
||||
let plugin = await findPlugin(appName);
|
||||
if (!plugin) {
|
||||
const fetched = await fetchPlugin(appName);
|
||||
if (!fetched.ok) {
|
||||
return { ok: false, appName, steps, error: fetched.error ?? `cannot fetch "${appName}"` };
|
||||
}
|
||||
await step(steps, onStep, `fetched: cloned into plugins/${appName}`);
|
||||
|
||||
// Re-discovered rather than assumed. The catalogue DESCRIBED this plugin; the manifest on disk is
|
||||
// what the platform actually obeys, and a catalogue that disagreed with it is a catalogue bug rather
|
||||
// than a second source of truth.
|
||||
plugin = await findPlugin(appName);
|
||||
if (!plugin) {
|
||||
return { ok: false, appName, steps, error: `cloned "${appName}" but it does not read as a plugin` };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// HOST BINARIES FIRST, before a table is created or a row is written.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user