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:
2026-08-15 18:21:51 +00:00
co-authored by Claude Opus 5
parent cca85a7524
commit dd26e4a688
14 changed files with 104 additions and 172 deletions
@@ -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;