example leaves too; plugins/ is documentation now
Done with `tea` as admin: plugins/offscale made public, plugins/example created public, both verified anonymously over https — which is the check that matters, because fetch.ts clones with no credentials and a private plugin simply cannot be installed from the marketplace. plugins/example 8 files, cloned back and diffed identical plugins/offscale now public (was private, so was uninstallable) `.gitignore` is one line now — `/plugins/*/`. The trailing slash is the whole design: it ignores DIRECTORIES, so files at the top of plugins/ stay tracked. The documentation about plugins belongs to the platform; the plugins do not. All three are in the marketplace catalogue, which is what makes this reversible: before this, untracking offscale meant a fresh install could never get it back. Now the store is the way in for all three. CatalogueEntry gains `icon` (a lucide name) alongside `iconUrl`. Both new entries ship glyphs rather than artwork, and without it they drew the generic puzzle piece in the store and their real icon after install — which reads as the icon changing rather than the store not knowing it. EXTRACTING-A-PLUGIN.md pointed at four directories a fresh checkout no longer has. It now gives repository URLs, says up front that this directory holds documentation rather than plugins, and carries a table of where each one went. Checked first that no tracked source statically imports a plugin directory — none does, so a fresh clone still builds. tsgo clean, frontend builds, 787 pass / 7 fail (unchanged). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,8 @@ pluginsRouter.get('/', async (ctx) => {
|
||||
label: e.label,
|
||||
summary: e.summary,
|
||||
image: e.iconUrl,
|
||||
// Same precedence the installed list uses: artwork wins, the glyph name is the fallback.
|
||||
...(e.iconUrl ? {} : { icon: e.icon }),
|
||||
tile: e.tile ?? 'badge',
|
||||
color: e.color,
|
||||
publisher: e.publisher,
|
||||
|
||||
@@ -50,6 +50,12 @@ export type CatalogueEntry = {
|
||||
* has never seen, so the catalogue carries a link rather than a file.
|
||||
*/
|
||||
iconUrl?: string;
|
||||
/**
|
||||
* A lucide NAME, for a plugin that ships no artwork. Mirrors `manifest.icon`, and the same precedence
|
||||
* applies: `iconUrl` wins where both are given. Without this a glyph plugin draws the generic puzzle
|
||||
* piece in the store and its real icon only after install, which reads as the icon having changed.
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Where to clone from. HTTPS, never SSH: a marketplace serves the same URL to every machine, and an
|
||||
* `ssh://` clone assumes a key on that host for that user — true on the machine that published the
|
||||
@@ -110,6 +116,40 @@ const CATALOGUE: CatalogueEntry[] = [
|
||||
],
|
||||
has: { api: true, schema: true, sidecar: true, web: true },
|
||||
},
|
||||
{
|
||||
appName: 'offscale',
|
||||
publisher: 'officerdev',
|
||||
version: '1.0.0',
|
||||
platform: '>=1.0.0',
|
||||
label: 'Offscale',
|
||||
summary: 'Your tailnet — machines, users, pre-auth keys, access policy and device invites',
|
||||
color: '#818cf8',
|
||||
icon: 'Network',
|
||||
source: 'https://gitea.officer.dev/plugins/offscale.git',
|
||||
permissions: [
|
||||
{
|
||||
key: 'offscale',
|
||||
label: 'Offscale',
|
||||
description: 'The tailnet: machines, routes, keys and ACLs',
|
||||
readOnlyWrites: ['/ssh-test', '/policy/assist'],
|
||||
},
|
||||
],
|
||||
has: { api: true, schema: true, sidecar: true, web: true },
|
||||
},
|
||||
{
|
||||
appName: 'example',
|
||||
publisher: 'officerdev',
|
||||
version: '1.0.0',
|
||||
platform: '>=1.0.0',
|
||||
label: 'Example',
|
||||
summary: 'The reference plugin — one route, nothing else',
|
||||
color: '#94a3b8',
|
||||
icon: 'Puzzle',
|
||||
source: 'https://gitea.officer.dev/plugins/example.git',
|
||||
permissions: [{ key: 'example', label: 'Example', description: 'The reference plugin' }],
|
||||
// No `db/` — the one part it deliberately omits, so the smallest real plugin stays the smallest.
|
||||
has: { api: true, schema: false, sidecar: true, web: true },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { resolveIcon } from '../../utils/resolve-icon';
|
||||
import {
|
||||
usePlugins,
|
||||
type AvailablePlugin,
|
||||
@@ -158,6 +159,13 @@ const ActionLog = ({
|
||||
);
|
||||
};
|
||||
|
||||
/** Artwork if the catalogue has a URL, else the plugin's own lucide glyph. */
|
||||
const AvailableGlyph = ({ plugin }: { plugin: AvailablePlugin }) => {
|
||||
if (plugin.image) return <img src={plugin.image} alt="" className="h-10 w-10 shrink-0 object-contain" />;
|
||||
const Glyph = resolveIcon(plugin.icon ?? 'Box');
|
||||
return <Glyph className="h-9 w-9 shrink-0" style={{ color: plugin.color }} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* A marketplace entry, before this machine has the code.
|
||||
*
|
||||
@@ -183,7 +191,7 @@ const AvailableDetail = ({
|
||||
}) => (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
{plugin.image ? <img src={plugin.image} alt="" className="h-10 w-10 shrink-0 object-contain" /> : null}
|
||||
<AvailableGlyph plugin={plugin} />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold text-duck-dark">{plugin.label}</h2>
|
||||
<p className="mt-0.5 text-sm text-duck-dark/60">{plugin.summary}</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { Puzzle, AlertTriangle } from 'lucide-react';
|
||||
import { usePlugins, type PluginItem } from './usePlugins';
|
||||
import { resolveIcon } from '../../utils/resolve-icon';
|
||||
|
||||
// The left panel: every plugin in the tree, installed or not.
|
||||
//
|
||||
@@ -40,10 +41,16 @@ export const PluginsList = () => {
|
||||
>
|
||||
{/* The plugin's own artwork when it ships some and is installed; otherwise the generic glyph
|
||||
this drew for everything before plugins could carry an icon. */}
|
||||
{/* Artwork, then the plugin's own glyph, then a generic one. The middle step matters: without
|
||||
it a glyph plugin shows a puzzle piece in the store and its real icon after install, which
|
||||
reads as the icon having changed rather than as the store not knowing it. */}
|
||||
{plugin.image ? (
|
||||
<img src={plugin.image} alt="" className="h-5 w-5 shrink-0 object-contain" />
|
||||
) : (
|
||||
<Puzzle className="h-4 w-4 shrink-0" style={{ color: plugin.color }} />
|
||||
(() => {
|
||||
const Glyph = plugin.icon ? resolveIcon(plugin.icon) : Puzzle;
|
||||
return <Glyph className="h-4 w-4 shrink-0" style={{ color: plugin.color }} />;
|
||||
})()
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-duck-dark">{plugin.label}</div>
|
||||
|
||||
@@ -94,6 +94,8 @@ export type AvailablePlugin = {
|
||||
summary: string;
|
||||
/** An absolute URL to the marketplace's copy — there is no local asset until it is installed. */
|
||||
image?: string;
|
||||
/** A lucide NAME, for a catalogue entry that ships no artwork. Only when `image` is absent. */
|
||||
icon?: string;
|
||||
tile: 'badge' | 'bare';
|
||||
color: string;
|
||||
publisher: string;
|
||||
|
||||
Reference in New Issue
Block a user