Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

305 linesCodeBlameRaw
1/**
2 * Marketplaces (docs.g1t.sh/guides/customize/, "Marketplaces"): where a
3 * workspace reads plugins from. Every workspace starts with the built-in
4 * ones (@g1t/contracts claude-plugin.ts `BUILTIN_MARKETPLACES`), whose
5 * manifests are cached once for everyone (workspace_id ''), and owners add
6 * more by the forms Claude Code takes. A manifest is read when the
7 * marketplace is added, again on demand, and again when it is older than
8 * `STALE_MS` and someone opens Discover.
9 *
10 * Discover is built from the cached manifests: one listing per plugin
11 * (`PluginListing`) with where g1t would fetch it from and whether it can
12 * (`fetchPlan`), and one per skill a plugin names outright
13 * (`SkillListing`), so a skills-only plugin lists its skills without a
14 * fetch. The rules and the tables are here, tested on SQLite; who may do
15 * what, the audit log and telling people are index.ts's.
16 */
17import type { FailureCode, Result } from "@g1t/contracts";
18import {
19 BUILTIN_MARKETPLACES,
20 type MarketplaceManifest,
21 type MarketplacePluginEntry,
22 type MarketplaceSource,
23 fetchPlan,
24 isReservedMarketplaceName,
25 marketplaceNameProblem,
26 parseMarketplaceManifest,
27 pluginRef,
28} from "../../../packages/contracts/src/claude-plugin.ts";
29import { MAX_MARKETPLACES, type Marketplace, type PluginAvailability, type PluginListing, type SkillListing } from "../../../packages/contracts/src/customize.ts";
30import { newId } from "../../../packages/contracts/src/ids.ts";
31import { type FetchDeps, fetchMarketplace } from "./customize-fetch.ts";
32
33const ok = <T>(value: T): Result<T> => ({ ok: true, value });
34const fail = (code: FailureCode, message: string): Result<never> => ({ ok: false, error: { code, message } });
35
36/** A cached manifest older than this is read again when Discover opens. */
37export const STALE_MS = 6 * 60 * 60 * 1000;
38
39export type MarketplaceRow = {
40 id: string;
41 workspace_id: string;
42 name: string;
43 source: string;
44 manifest: string | null;
45 fetched_at: string | null;
46 problem: string | null;
47 added_by: string;
48 added_at: string;
49};
50
51/** g1t's own marketplace, in code: what g1t builds and will build, under its own name. */
52export const G1T_MARKETPLACE: MarketplaceManifest = {
53 name: "g1t",
54 owner: { name: "g1t" },
55 description: "g1t's own plugins: the pages, cards and roles g1t is building for what a team does besides code.",
56 version: null,
57 pluginRoot: null,
58 plugins: [],
59 problems: [],
60};
61
62export type ManifestCache = { manifest: MarketplaceManifest; sha: string | null; fetched_at: string };
63
64function json<T>(text: string | null, fallback: T): T {
65 if (!text) return fallback;
66 try {
67 return JSON.parse(text) as T;
68 } catch {
69 return fallback;
70 }
71}
72
73/** The source a row keeps. */
74export function sourceOf(row: MarketplaceRow): MarketplaceSource {
75 return json<MarketplaceSource>(row.source, { source: "builtin", id: row.name });
76}
77
78/** The cached manifest a row keeps, parsed again (the cache holds the raw JSON, so parsing improvements apply). */
79export function cachedManifest(row: MarketplaceRow): MarketplaceManifest | null {
80 const cache = json<{ json: unknown; sha: string | null } | null>(row.manifest, null);
81 if (!cache) return null;
82 const parsed = parseMarketplaceManifest(cache.json);
83 return parsed.ok ? parsed.value : null;
84}
85
86/** The marketplaces a workspace reads: the built-in ones (from the shared cache rows), then its own, by name. */
87export async function listRows(db: D1Database, workspaceId: string): Promise<{ row: MarketplaceRow | null; builtin: (typeof BUILTIN_MARKETPLACES)[number] | null; name: string }[]> {
88 const { results } = await db.prepare("SELECT * FROM marketplaces WHERE workspace_id IN ('', ?) ORDER BY added_at").bind(workspaceId).all<MarketplaceRow>();
89 const shared = new Map(results.filter((r) => r.workspace_id === "").map((r) => [r.name, r]));
90 const own = results.filter((r) => r.workspace_id === workspaceId);
91 return [...BUILTIN_MARKETPLACES.map((b) => ({ row: shared.get(b.name) ?? null, builtin: b, name: b.name })), ...own.map((r) => ({ row: r, builtin: null, name: r.name }))];
92}
93
94/** Reads a marketplace's manifest and caches it on its row (a built-in's row is made if missing). */
95export async function refreshRow(
96 db: D1Database,
97 entry: { row: MarketplaceRow | null; builtin: (typeof BUILTIN_MARKETPLACES)[number] | null; name: string },
98 deps: FetchDeps,
99 host: string | null,
100 now = new Date(),
101): Promise<MarketplaceRow> {
102 const source = entry.builtin ? entry.builtin.source : sourceOf(entry.row!);
103 const read = await fetchMarketplace(source, deps, host);
104 const at = now.toISOString();
105 let manifest: string | null = entry.row?.manifest ?? null;
106 let problem: string | null = null;
107 if (read.ok) {
108 const parsed = parseMarketplaceManifest(read.json);
109 if (parsed.ok) {
110 if (!entry.builtin && parsed.value.name !== entry.name) problem = `Its marketplace.json calls itself ${parsed.value.name}, not ${entry.name}.`;
111 manifest = JSON.stringify({ json: read.json, sha: read.sha });
112 } else problem = parsed.message;
113 } else problem = read.message;
114 if (!entry.row) {
115 const row: MarketplaceRow = { id: newId("mkt", now.getTime()), workspace_id: "", name: entry.name, source: JSON.stringify(source), manifest, fetched_at: at, problem, added_by: "g1t", added_at: at };
116 await db
117 .prepare("INSERT INTO marketplaces (id, workspace_id, name, source, manifest, fetched_at, problem, added_by, added_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (workspace_id, name) DO UPDATE SET manifest = excluded.manifest, fetched_at = excluded.fetched_at, problem = excluded.problem")
118 .bind(row.id, row.workspace_id, row.name, row.source, row.manifest, row.fetched_at, row.problem, row.added_by, row.added_at)
119 .run();
120 return row;
121 }
122 await db.prepare("UPDATE marketplaces SET manifest = ?, fetched_at = ?, problem = ? WHERE id = ?").bind(manifest, at, problem, entry.row.id).run();
123 return { ...entry.row, manifest, fetched_at: at, problem };
124}
125
126/** Every marketplace with a fresh enough manifest, reading the stale ones (and the built-ins' first time). */
127export async function loadAll(db: D1Database, workspaceId: string, deps: FetchDeps, host: string | null, now = new Date()): Promise<{ row: MarketplaceRow; builtin: boolean; manifest: MarketplaceManifest | null }[]> {
128 const out: { row: MarketplaceRow; builtin: boolean; manifest: MarketplaceManifest | null }[] = [];
129 for (const entry of await listRows(db, workspaceId)) {
130 let row = entry.row;
131 const stale = !row?.fetched_at || now.getTime() - Date.parse(row.fetched_at) > STALE_MS;
132 if (stale) {
133 try {
134 row = await refreshRow(db, entry, deps, host, now);
135 } catch (error) {
136 console.error("agents: a marketplace wasn't read", entry.name, String(error));
137 }
138 }
139 if (!row) continue;
140 out.push({ row, builtin: !!entry.builtin, manifest: cachedManifest(row) });
141 }
142 return out;
143}
144
145/** The marketplaces as their page lists them. */
146export function marketplacesOut(loaded: { row: MarketplaceRow; builtin: boolean; manifest: MarketplaceManifest | null }[], owner: boolean): Marketplace[] {
147 const out = loaded.map(({ row, builtin, manifest }) => ({
148 name: row.name,
149 source: sourceOf(row),
150 builtin,
151 about: builtin ? (BUILTIN_MARKETPLACES.find((b) => b.name === row.name)?.about ?? null) : (manifest?.description ?? null),
152 owner: manifest?.owner.name ?? null,
153 plugins: manifest?.plugins.length ?? 0,
154 fetched_at: row.fetched_at,
155 problem: row.problem,
156 added_by: builtin ? null : row.added_by,
157 added_at: builtin ? null : row.added_at,
158 can_remove: owner && !builtin,
159 }));
160 out.push({
161 name: G1T_MARKETPLACE.name,
162 source: { source: "builtin", id: "g1t" },
163 builtin: true,
164 about: G1T_MARKETPLACE.description,
165 owner: "g1t",
166 plugins: G1T_MARKETPLACE.plugins.length,
167 fetched_at: null,
168 problem: null,
169 added_by: null,
170 added_at: null,
171 can_remove: false,
172 });
173 return out;
174}
175
176/** Adds a marketplace for the workspace and reads it; the row is kept even when the first read fails, so it can be read again. */
177export async function addMarketplace(
178 db: D1Database,
179 workspaceId: string,
180 name: string | null,
181 source: MarketplaceSource,
182 by: string,
183 deps: FetchDeps,
184 host: string | null,
185 now = new Date(),
186): Promise<Result<Marketplace>> {
187 const count = await db.prepare("SELECT COUNT(*) AS n FROM marketplaces WHERE workspace_id = ?").bind(workspaceId).first<{ n: number }>();
188 if ((count?.n ?? 0) >= MAX_MARKETPLACES) return fail("limit", `A workspace reads at most ${MAX_MARKETPLACES} marketplaces of its own.`);
189 const read = await fetchMarketplace(source, deps, host);
190 if (!read.ok) return fail("invalid", read.message);
191 const parsed = parseMarketplaceManifest(read.json);
192 if (!parsed.ok) return fail("invalid", `Its marketplace.json can't be read: ${parsed.message}`);
193 const wanted = name?.trim() || parsed.value.name;
194 const problem = marketplaceNameProblem(wanted);
195 if (problem) return fail("invalid", problem);
196 if (isReservedMarketplaceName(wanted) || wanted === G1T_MARKETPLACE.name) return fail("invalid", `${wanted} is a name kept for a built-in marketplace. Give it another name here.`);
197 const at = now.toISOString();
198 const row: MarketplaceRow = { id: newId("mkt", now.getTime()), workspace_id: workspaceId, name: wanted, source: JSON.stringify(source), manifest: JSON.stringify({ json: read.json, sha: read.sha }), fetched_at: at, problem: null, added_by: by, added_at: at };
199 try {
200 await db
201 .prepare("INSERT INTO marketplaces (id, workspace_id, name, source, manifest, fetched_at, problem, added_by, added_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
202 .bind(row.id, row.workspace_id, row.name, row.source, row.manifest, row.fetched_at, row.problem, row.added_by, row.added_at)
203 .run();
204 } catch (error) {
205 if (String(error).includes("UNIQUE")) return fail("conflict", `The workspace already reads a marketplace called ${wanted}.`);
206 throw error;
207 }
208 return ok(marketplacesOut([{ row, builtin: false, manifest: parsed.value }], true)[0]!);
209}
210
211/** Removes one of the workspace's own marketplaces. Plugins installed from it stay installed. */
212export async function removeMarketplace(db: D1Database, workspaceId: string, name: string): Promise<Result<null>> {
213 if (BUILTIN_MARKETPLACES.some((b) => b.name === name) || name === G1T_MARKETPLACE.name) return fail("invalid", `${name} is built in, so it can't be removed.`);
214 const done = await db.prepare("DELETE FROM marketplaces WHERE workspace_id = ? AND name = ?").bind(workspaceId, name).run();
215 return done.meta.changes ? ok(null) : fail("not_found", "The workspace reads no marketplace by that name.");
216}
217
218/** One marketplace's row by name, the workspace's own or a built-in's shared cache. */
219export async function marketplaceByName(db: D1Database, workspaceId: string, name: string): Promise<{ row: MarketplaceRow | null; builtin: (typeof BUILTIN_MARKETPLACES)[number] | null; name: string } | null> {
220 const entries = await listRows(db, workspaceId);
221 return entries.find((e) => e.name === name) ?? null;
222}
223
224// ── Discover ─────────────────────────────────────────────────────────────
225
226/** The installs Discover marks, by ref. */
227export type InstallMarks = Map<string, { enabled: boolean }>;
228
229/** A listing for one plugin entry of one marketplace. */
230export function listingOf(entry: MarketplacePluginEntry, marketplace: { name: string; source: MarketplaceSource; pluginRoot: string | null }, installs: InstallMarks, host: string | null): PluginListing {
231 const ref = pluginRef(entry.name, marketplace.name);
232 const plan = fetchPlan(entry.source, marketplace.source, { pluginRoot: marketplace.pluginRoot, host });
233 const installed = installs.get(ref);
234 const soon = entry.extra.status === "soon";
235 const availability: PluginAvailability = installed ? { kind: "installed", enabled: installed.enabled } : soon ? { kind: "soon" } : plan.kind === "unsupported" ? { kind: "unavailable", why: plan.why } : { kind: "available" };
236 return {
237 ref,
238 name: entry.name,
239 marketplace: marketplace.name,
240 display_name: entry.displayName,
241 description: entry.description,
242 version: entry.version,
243 author: entry.author?.name ?? null,
244 homepage: entry.homepage,
245 category: entry.category,
246 tags: entry.tags,
247 keywords: entry.keywords,
248 fetch: plan,
249 availability,
250 skills: entry.skills.filter((s) => s !== ".").map((s) => s.replace(/^.*\//, "")),
251 };
252}
253
254/** Every plugin every marketplace lists, g1t's own last, in listing order. */
255export function discoverPlugins(loaded: { row: MarketplaceRow; manifest: MarketplaceManifest | null }[], installs: InstallMarks, host: string | null): PluginListing[] {
256 const out: PluginListing[] = [];
257 for (const { row, manifest } of loaded) {
258 if (!manifest) continue;
259 const source = sourceOf(row);
260 for (const entry of manifest.plugins) out.push(listingOf(entry, { name: row.name, source, pluginRoot: manifest.pluginRoot }, installs, host));
261 }
262 for (const entry of G1T_MARKETPLACE.plugins) out.push(listingOf(entry, { name: G1T_MARKETPLACE.name, source: { source: "builtin", id: "g1t" }, pluginRoot: null }, installs, host));
263 return out;
264}
265
266/**
267 * Every skill a marketplace names outright in a plugin entry (skills-only
268 * plugins list theirs), one listing each, with whether the library has it
269 * from that plugin already (`added`: by `plugin:name`).
270 */
271export function discoverSkills(listings: PluginListing[], added: ReadonlySet<string>): SkillListing[] {
272 const out: SkillListing[] = [];
273 for (const plugin of listings) {
274 for (const name of plugin.skills) {
275 out.push({
276 ref: `${plugin.name}:${name}`,
277 name,
278 plugin: plugin.name,
279 marketplace: plugin.marketplace,
280 description: plugin.description,
281 category: plugin.category,
282 author: plugin.author,
283 added: added.has(`${plugin.ref}/${name}`),
284 availability: plugin.availability,
285 });
286 }
287 }
288 return out;
289}
290
291/** A plugin's listing by `name@marketplace`, from what is loaded. */
292export function findListing(listings: PluginListing[], ref: string): PluginListing | null {
293 return listings.find((l) => l.ref === ref) ?? null;
294}
295
296/** The entry behind a listing, for installing: the marketplace's parsed entry and source. */
297export function entryOf(loaded: { row: MarketplaceRow; manifest: MarketplaceManifest | null }[], ref: { name: string; marketplace: string }): { entry: MarketplacePluginEntry; source: MarketplaceSource; pluginRoot: string | null } | null {
298 if (ref.marketplace === G1T_MARKETPLACE.name) {
299 const entry = G1T_MARKETPLACE.plugins.find((p) => p.name === ref.name);
300 return entry ? { entry, source: { source: "builtin", id: "g1t" }, pluginRoot: null } : null;
301 }
302 const found = loaded.find((l) => l.row.name === ref.marketplace);
303 const entry = found?.manifest?.plugins.find((p) => p.name === ref.name);
304 return found && entry ? { entry, source: sourceOf(found.row), pluginRoot: found.manifest!.pluginRoot } : null;
305}