@donkeylabs/server 2.0.19 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1064 @@
1
+ # Plugin Registry System Design
2
+
3
+ A design document for a centralized plugin registry to enable discovery, sharing, and distribution of DonkeyLabs plugins.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Architecture](#architecture)
9
+ - [Registry API](#registry-api)
10
+ - [CLI Integration](#cli-integration)
11
+ - [Plugin Discovery](#plugin-discovery)
12
+ - [Versioning & Dependencies](#versioning--dependencies)
13
+ - [Security & Validation](#security--validation)
14
+ - [Implementation Roadmap](#implementation-roadmap)
15
+
16
+ ---
17
+
18
+ ## Overview
19
+
20
+ ### Problem Statement
21
+
22
+ Currently, plugins are:
23
+ - Hard to discover (no centralized repository)
24
+ - Difficult to share (manual copy-paste)
25
+ - No versioning system
26
+ - No dependency resolution
27
+ - No quality standards
28
+
29
+ ### Goals
30
+
31
+ 1. **Discoverability** - Search and browse available plugins
32
+ 2. **Easy Installation** - One-command plugin installation
33
+ 3. **Version Management** - Semantic versioning and updates
34
+ 4. **Dependency Resolution** - Auto-install dependencies
35
+ 5. **Quality Assurance** - Verified plugins with tests
36
+ 6. **Community** - Ratings, reviews, and contributions
37
+
38
+ ### Use Cases
39
+
40
+ **Developer A:** Wants to add authentication to their app
41
+ ```bash
42
+ donkeylabs plugin search auth
43
+ # Shows: auth-jwt, auth-oauth, auth-magic-link
44
+ donkeylabs plugin install auth-jwt
45
+ ```
46
+
47
+ **Developer B:** Built a stripe plugin, wants to share
48
+ ```bash
49
+ donkeylabs plugin publish ./plugins/stripe
50
+ # Plugin uploaded to registry, others can install
51
+ ```
52
+
53
+ **Developer C:** Updating dependencies
54
+ ```bash
55
+ donkeylabs plugin update
56
+ # Checks for updates, resolves conflicts
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Architecture
62
+
63
+ ### System Components
64
+
65
+ ```
66
+ ┌─────────────────────────────────────────────────────────────┐
67
+ │ Plugin Registry │
68
+ ├─────────────────────────────────────────────────────────────┤
69
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
70
+ │ │ Web UI │ │ Registry │ │ Package │ │
71
+ │ │ (Next.js) │ │ API │ │ Storage │ │
72
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
73
+ │ │ │ │ │
74
+ │ └──────────────────┼──────────────────┘ │
75
+ │ │ │
76
+ │ ┌──────────────┐ ┌──────┴────────┐ ┌──────────────┐ │
77
+ │ │ Search │ │ Database │ │ CDN │ │
78
+ │ │ (Algolia) │ │ (PostgreSQL) │ │ (CloudFront)│ │
79
+ │ └──────────────┘ └───────────────┘ └──────────────┘ │
80
+ └─────────────────────────────────────────────────────────────┘
81
+
82
+ ┌─────────────────────┼─────────────────────┐
83
+ │ │ │
84
+ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
85
+ │ CLI │ │ CLI │ │ CLI │
86
+ │ User A │ │ User B │ │ User C │
87
+ └─────────┘ └─────────┘ └─────────┘
88
+ ```
89
+
90
+ ### Registry API Endpoints
91
+
92
+ ```typescript
93
+ // Registry API Specification
94
+
95
+ interface RegistryAPI {
96
+ // Search plugins
97
+ "GET /api/plugins": {
98
+ query: { q?: string; category?: string; sort?: "downloads" | "rating" | "recent" };
99
+ response: PaginatedResponse<PluginSummary>;
100
+ };
101
+
102
+ // Get plugin details
103
+ "GET /api/plugins/:name": {
104
+ response: PluginDetails;
105
+ };
106
+
107
+ // Get plugin versions
108
+ "GET /api/plugins/:name/versions": {
109
+ response: PluginVersion[];
110
+ };
111
+
112
+ // Download plugin package
113
+ "GET /api/plugins/:name/:version/download": {
114
+ response: Blob; // .tar.gz package
115
+ };
116
+
117
+ // Publish plugin (authenticated)
118
+ "POST /api/plugins": {
119
+ body: FormData; // package.tar.gz + metadata
120
+ headers: { Authorization: string };
121
+ response: { success: boolean; plugin: PluginSummary };
122
+ };
123
+
124
+ // Update plugin (authenticated)
125
+ "PUT /api/plugins/:name": {
126
+ body: FormData;
127
+ headers: { Authorization: string };
128
+ response: { success: boolean };
129
+ };
130
+
131
+ // Rate plugin (authenticated)
132
+ "POST /api/plugins/:name/ratings": {
133
+ body: { rating: 1-5; review?: string };
134
+ headers: { Authorization: string };
135
+ };
136
+ }
137
+ ```
138
+
139
+ ### Data Models
140
+
141
+ ```typescript
142
+ // Plugin entity
143
+ interface Plugin {
144
+ id: string;
145
+ name: string; // e.g., "auth-jwt"
146
+ displayName: string; // e.g., "JWT Authentication"
147
+ description: string;
148
+ author: {
149
+ name: string;
150
+ email: string;
151
+ github?: string;
152
+ };
153
+ repository?: string; // GitHub URL
154
+ license: string;
155
+ categories: string[]; // ["auth", "security"]
156
+ tags: string[]; // ["jwt", "authentication"]
157
+
158
+ // Stats
159
+ downloads: number;
160
+ rating: number; // 0-5 average
161
+ ratingCount: number;
162
+
163
+ // Versions
164
+ versions: PluginVersion[];
165
+ latestVersion: string;
166
+
167
+ // Metadata
168
+ createdAt: Date;
169
+ updatedAt: Date;
170
+ verified: boolean; // Official/community
171
+ }
172
+
173
+ interface PluginVersion {
174
+ version: string; // semver
175
+ description?: string; // changelog entry
176
+ deprecated?: boolean;
177
+
178
+ // Dependencies
179
+ dependencies: {
180
+ plugins?: string[]; // ["users@^1.0.0"]
181
+ packages?: Record<string, string>; // npm deps
182
+ };
183
+
184
+ // Package info
185
+ packageUrl: string;
186
+ checksum: string; // sha256
187
+ size: number; // bytes
188
+
189
+ // Compatibility
190
+ engine: {
191
+ node?: string;
192
+ bun?: string;
193
+ };
194
+ framework: string; // @donkeylabs/server version range
195
+
196
+ createdAt: Date;
197
+ }
198
+
199
+ // Search index document
200
+ interface PluginSearchDoc {
201
+ objectID: string; // plugin name
202
+ name: string;
203
+ displayName: string;
204
+ description: string;
205
+ categories: string[];
206
+ tags: string[];
207
+ author: string;
208
+ downloads: number;
209
+ rating: number;
210
+ verified: boolean;
211
+ _tags: string[];
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Registry API
218
+
219
+ ### Core Endpoints
220
+
221
+ ```typescript
222
+ // packages/registry/src/server.ts
223
+ import { createRouter, defineRoute } from "@donkeylabs/server";
224
+ import { z } from "zod";
225
+
226
+ export const registryRouter = createRouter("registry");
227
+
228
+ // List/search plugins
229
+ registryRouter.route("plugins.list").typed(defineRoute({
230
+ input: z.object({
231
+ q: z.string().optional(),
232
+ category: z.string().optional(),
233
+ tag: z.string().optional(),
234
+ author: z.string().optional(),
235
+ sort: z.enum(["downloads", "rating", "recent", "name"]).default("downloads"),
236
+ page: z.number().default(1),
237
+ limit: z.number().max(50).default(20),
238
+ }),
239
+ output: z.object({
240
+ plugins: z.array(pluginSummarySchema),
241
+ pagination: paginationSchema,
242
+ }),
243
+ handle: async (input, ctx) => {
244
+ const query = ctx.plugins.search.buildQuery(input);
245
+ const results = await ctx.plugins.search.execute(query);
246
+ return results;
247
+ },
248
+ }));
249
+
250
+ // Get plugin details
251
+ registryRouter.route("plugins.get").typed(defineRoute({
252
+ input: z.object({ name: z.string() }),
253
+ output: pluginDetailsSchema,
254
+ handle: async (input, ctx) => {
255
+ const plugin = await ctx.plugins.store.getByName(input.name);
256
+ if (!plugin) throw ctx.errors.NotFound("Plugin not found");
257
+ return plugin;
258
+ },
259
+ }));
260
+
261
+ // Download plugin
262
+ registryRouter.route("plugins.download").stream({
263
+ input: z.object({
264
+ name: z.string(),
265
+ version: z.string(),
266
+ }),
267
+ handle: async (input, ctx) => {
268
+ const version = await ctx.plugins.store.getVersion(input.name, input.version);
269
+ if (!version) throw ctx.errors.NotFound("Version not found");
270
+
271
+ // Stream from storage
272
+ const stream = await ctx.plugins.storage.download(version.packageUrl);
273
+
274
+ return {
275
+ stream,
276
+ headers: {
277
+ "Content-Type": "application/gzip",
278
+ "Content-Disposition": `attachment; filename="${input.name}-${input.version}.tar.gz"`,
279
+ },
280
+ };
281
+ },
282
+ });
283
+
284
+ // Publish plugin (authenticated)
285
+ registryRouter.route("plugins.publish").typed(defineRoute({
286
+ input: z.object({
287
+ name: z.string().regex(/^[a-z0-9-]+$/),
288
+ version: z.string(),
289
+ description: z.string(),
290
+ categories: z.array(z.string()),
291
+ tags: z.array(z.string()),
292
+ repository: z.string().url().optional(),
293
+ license: z.string(),
294
+ dependencies: z.object({
295
+ plugins: z.array(z.string()).optional(),
296
+ packages: z.record(z.string()).optional(),
297
+ }).optional(),
298
+ // Package uploaded as multipart form
299
+ }),
300
+ output: z.object({
301
+ success: z.boolean(),
302
+ plugin: pluginSummarySchema,
303
+ }),
304
+ handle: async (input, ctx) => {
305
+ // Verify authentication
306
+ const user = await ctx.plugins.auth.verifyToken(ctx.request);
307
+
308
+ // Validate package
309
+ const validation = await ctx.plugins.validator.validate(input.package);
310
+ if (!validation.valid) {
311
+ throw ctx.errors.BadRequest(validation.errors.join(", "));
312
+ }
313
+
314
+ // Store package
315
+ const packageUrl = await ctx.plugins.storage.upload(
316
+ input.name,
317
+ input.version,
318
+ input.package
319
+ );
320
+
321
+ // Create version record
322
+ const version = await ctx.plugins.store.createVersion({
323
+ pluginName: input.name,
324
+ version: input.version,
325
+ description: input.description,
326
+ dependencies: input.dependencies,
327
+ packageUrl,
328
+ checksum: validation.checksum,
329
+ size: validation.size,
330
+ });
331
+
332
+ // Update search index
333
+ await ctx.plugins.search.indexPlugin(input.name);
334
+
335
+ return { success: true, plugin: await ctx.plugins.store.getByName(input.name) };
336
+ },
337
+ }));
338
+ ```
339
+
340
+ ### Search Implementation
341
+
342
+ ```typescript
343
+ // Using Algolia for full-text search
344
+
345
+ class PluginSearchService {
346
+ private client: algoliasearch.Client;
347
+ private index: algoliasearch.Index;
348
+
349
+ constructor() {
350
+ this.client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY);
351
+ this.index = this.client.initIndex("plugins");
352
+ }
353
+
354
+ async buildQuery(params: SearchParams): algoliasearch.SearchOptions {
355
+ const filters = [];
356
+
357
+ if (params.category) {
358
+ filters.push(`categories:${params.category}`);
359
+ }
360
+ if (params.verified) {
361
+ filters.push("verified:true");
362
+ }
363
+
364
+ return {
365
+ query: params.q,
366
+ filters: filters.join(" AND "),
367
+ page: params.page - 1,
368
+ hitsPerPage: params.limit,
369
+ attributesToHighlight: ["name", "description"],
370
+ highlightPreTag: "<mark>",
371
+ highlightPostTag: "</mark>",
372
+ };
373
+ }
374
+
375
+ async execute(options: algoliasearch.SearchOptions) {
376
+ const { hits, nbHits, nbPages, page } = await this.index.search(options);
377
+
378
+ return {
379
+ plugins: hits.map(this.transformHit),
380
+ pagination: {
381
+ total: nbHits,
382
+ pages: nbPages,
383
+ current: page + 1,
384
+ hasMore: page + 1 < nbPages,
385
+ },
386
+ };
387
+ }
388
+
389
+ async indexPlugin(plugin: Plugin) {
390
+ const doc: PluginSearchDoc = {
391
+ objectID: plugin.name,
392
+ name: plugin.name,
393
+ displayName: plugin.displayName,
394
+ description: plugin.description,
395
+ categories: plugin.categories,
396
+ tags: plugin.tags,
397
+ author: plugin.author.name,
398
+ downloads: plugin.downloads,
399
+ rating: plugin.rating,
400
+ verified: plugin.verified,
401
+ };
402
+
403
+ await this.index.saveObject(doc);
404
+ }
405
+ }
406
+ ```
407
+
408
+ ---
409
+
410
+ ## CLI Integration
411
+
412
+ ### New CLI Commands
413
+
414
+ ```typescript
415
+ // packages/cli/src/commands/registry.ts
416
+
417
+ export const registryCommands = {
418
+ // Search for plugins
419
+ async search(query: string, options: SearchOptions) {
420
+ const registry = new RegistryClient(REGISTRY_URL);
421
+ const results = await registry.search({
422
+ q: query,
423
+ category: options.category,
424
+ sort: options.sort,
425
+ });
426
+
427
+ // Display results
428
+ console.log(`\nFound ${results.pagination.total} plugins:\n`);
429
+ results.plugins.forEach((plugin) => {
430
+ console.log(`${pc.cyan(plugin.name)} ${pc.gray(plugin.latestVersion)}`);
431
+ console.log(` ${plugin.description}`);
432
+ console.log(` ⭐ ${plugin.rating} | ⬇️ ${plugin.downloads} downloads`);
433
+ console.log(` ${pc.gray(plugin.categories.join(", "))}\n`);
434
+ });
435
+ },
436
+
437
+ // Install a plugin
438
+ async install(name: string, options: InstallOptions) {
439
+ const registry = new RegistryClient(REGISTRY_URL);
440
+
441
+ // Check if already installed
442
+ const existing = await this.getInstalledPlugin(name);
443
+ if (existing) {
444
+ console.log(pc.yellow(`⚠️ ${name} is already installed (${existing.version})`));
445
+ if (!options.force) return;
446
+ }
447
+
448
+ // Fetch plugin info
449
+ const plugin = await registry.getPlugin(name);
450
+ const version = options.version || plugin.latestVersion;
451
+
452
+ console.log(pc.blue(`⬇️ Downloading ${name}@${version}...`));
453
+
454
+ // Download package
455
+ const packageBuffer = await registry.download(name, version);
456
+
457
+ // Resolve dependencies
458
+ const deps = await this.resolveDependencies(plugin.versions.find(v => v.version === version)!);
459
+
460
+ if (deps.length > 0) {
461
+ console.log(pc.blue(`📦 Installing ${deps.length} dependencies...`));
462
+ for (const dep of deps) {
463
+ await this.install(dep.name, { version: dep.version, silent: true });
464
+ }
465
+ }
466
+
467
+ // Extract and install
468
+ await this.extractPackage(name, version, packageBuffer);
469
+
470
+ // Run post-install hooks
471
+ await this.runPostInstall(name);
472
+
473
+ // Update donkeylabs.config.ts
474
+ await this.updateConfig(name);
475
+
476
+ console.log(pc.green(`✅ ${name}@${version} installed successfully!`));
477
+
478
+ // Show next steps
479
+ console.log(pc.gray(`\nNext steps:`));
480
+ console.log(pc.gray(` 1. Import the plugin in your server`));
481
+ console.log(pc.gray(` 2. Register it with server.registerPlugin(${name}Plugin)`));
482
+ console.log(pc.gray(` 3. Run bun run gen:types`));
483
+ },
484
+
485
+ // Update plugins
486
+ async update(options: UpdateOptions) {
487
+ const installed = await this.getInstalledPlugins();
488
+
489
+ const updates = [];
490
+ for (const plugin of installed) {
491
+ const latest = await registry.getLatestVersion(plugin.name);
492
+ if (semver.gt(latest, plugin.version)) {
493
+ updates.push({ name: plugin.name, current: plugin.version, latest });
494
+ }
495
+ }
496
+
497
+ if (updates.length === 0) {
498
+ console.log(pc.green("✅ All plugins are up to date!"));
499
+ return;
500
+ }
501
+
502
+ console.log(pc.blue(`\n${updates.length} update(s) available:\n`));
503
+ updates.forEach((u) => {
504
+ console.log(`${u.name}: ${pc.gray(u.current)} → ${pc.cyan(u.latest)}`);
505
+ });
506
+
507
+ if (options.dryRun) return;
508
+
509
+ // Install updates
510
+ for (const update of updates) {
511
+ await this.install(update.name, { version: update.latest });
512
+ }
513
+ },
514
+
515
+ // Publish plugin
516
+ async publish(pluginPath: string, options: PublishOptions) {
517
+ // Validate plugin structure
518
+ const validation = await this.validatePlugin(pluginPath);
519
+ if (!validation.valid) {
520
+ console.error(pc.red("❌ Validation failed:"));
521
+ validation.errors.forEach((e) => console.error(` - ${e}`));
522
+ return;
523
+ }
524
+
525
+ // Get version from package.json or prompt
526
+ const version = await this.getVersion(pluginPath, options.version);
527
+
528
+ // Build package
529
+ console.log(pc.blue("📦 Building package..."));
530
+ const packageBuffer = await this.buildPackage(pluginPath);
531
+
532
+ // Get auth token
533
+ const token = await this.getAuthToken();
534
+
535
+ // Publish
536
+ console.log(pc.blue(`🚀 Publishing to registry...`));
537
+ const registry = new RegistryClient(REGISTRY_URL, token);
538
+
539
+ await registry.publish({
540
+ name: validation.name,
541
+ version,
542
+ description: validation.description,
543
+ categories: validation.categories,
544
+ tags: validation.tags,
545
+ repository: validation.repository,
546
+ license: validation.license,
547
+ dependencies: validation.dependencies,
548
+ package: packageBuffer,
549
+ });
550
+
551
+ console.log(pc.green(`✅ Published ${validation.name}@${version}!`));
552
+ },
553
+
554
+ // Uninstall plugin
555
+ async uninstall(name: string, options: UninstallOptions) {
556
+ // Check for dependent plugins
557
+ const dependents = await this.getDependents(name);
558
+ if (dependents.length > 0 && !options.force) {
559
+ console.error(pc.red(`❌ Cannot uninstall: used by ${dependents.join(", ")}`));
560
+ return;
561
+ }
562
+
563
+ // Remove from filesystem
564
+ await this.removePlugin(name);
565
+
566
+ // Update config
567
+ await this.removeFromConfig(name);
568
+
569
+ console.log(pc.green(`✅ ${name} uninstalled`));
570
+ },
571
+ };
572
+ ```
573
+
574
+ ### Registry Client
575
+
576
+ ```typescript
577
+ // packages/cli/src/registry/client.ts
578
+
579
+ class RegistryClient {
580
+ private baseUrl: string;
581
+ private token?: string;
582
+
583
+ constructor(baseUrl: string, token?: string) {
584
+ this.baseUrl = baseUrl;
585
+ this.token = token;
586
+ }
587
+
588
+ async search(params: SearchParams): Promise<SearchResults> {
589
+ const query = new URLSearchParams();
590
+ if (params.q) query.set("q", params.q);
591
+ if (params.category) query.set("category", params.category);
592
+
593
+ const res = await fetch(`${this.baseUrl}/api/plugins?${query}`);
594
+ if (!res.ok) throw new Error(`Search failed: ${res.statusText}`);
595
+ return res.json();
596
+ }
597
+
598
+ async getPlugin(name: string): Promise<Plugin> {
599
+ const res = await fetch(`${this.baseUrl}/api/plugins/${name}`);
600
+ if (!res.ok) throw new Error(`Plugin not found: ${name}`);
601
+ return res.json();
602
+ }
603
+
604
+ async download(name: string, version: string): Promise<Buffer> {
605
+ const res = await fetch(
606
+ `${this.baseUrl}/api/plugins/${name}/${version}/download`
607
+ );
608
+ if (!res.ok) throw new Error(`Download failed: ${res.statusText}`);
609
+ return Buffer.from(await res.arrayBuffer());
610
+ }
611
+
612
+ async publish(data: PublishData): Promise<PublishResult> {
613
+ const form = new FormData();
614
+ form.append("name", data.name);
615
+ form.append("version", data.version);
616
+ form.append("description", data.description);
617
+ form.append("categories", JSON.stringify(data.categories));
618
+ form.append("tags", JSON.stringify(data.tags));
619
+ form.append("package", new Blob([data.package]), "package.tar.gz");
620
+
621
+ const res = await fetch(`${this.baseUrl}/api/plugins`, {
622
+ method: "POST",
623
+ headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
624
+ body: form,
625
+ });
626
+
627
+ if (!res.ok) throw new Error(`Publish failed: ${res.statusText}`);
628
+ return res.json();
629
+ }
630
+ }
631
+ ```
632
+
633
+ ---
634
+
635
+ ## Plugin Discovery
636
+
637
+ ### Web Interface
638
+
639
+ ```typescript
640
+ // Registry web UI (Next.js)
641
+ // app/page.tsx - Plugin discovery homepage
642
+
643
+ export default async function HomePage() {
644
+ const featured = await getFeaturedPlugins();
645
+ const popular = await getPopularPlugins();
646
+ const recent = await getRecentPlugins();
647
+
648
+ return (
649
+ <div className="container mx-auto px-4 py-8">
650
+ {/* Hero */}
651
+ <section className="text-center py-16">
652
+ <h1 className="text-4xl font-bold mb-4">
653
+ DonkeyLabs Plugin Registry
654
+ </h1>
655
+ <p className="text-xl text-gray-600 mb-8">
656
+ Discover and share plugins for the DonkeyLabs framework
657
+ </p>
658
+ <SearchBox />
659
+ </section>
660
+
661
+ {/* Categories */}
662
+ <section className="py-8">
663
+ <h2 className="text-2xl font-semibold mb-4">Categories</h2>
664
+ <div className="flex flex-wrap gap-2">
665
+ {CATEGORIES.map((cat) => (
666
+ <CategoryBadge key={cat} name={cat} />
667
+ ))}
668
+ </div>
669
+ </section>
670
+
671
+ {/* Featured */}
672
+ <section className="py-8">
673
+ <h2 className="text-2xl font-semibold mb-4">Featured Plugins</h2>
674
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
675
+ {featured.map((plugin) => (
676
+ <PluginCard key={plugin.name} plugin={plugin} featured />
677
+ ))}
678
+ </div>
679
+ </section>
680
+
681
+ {/* Popular */}
682
+ <section className="py-8">
683
+ <h2 className="text-2xl font-semibold mb-4">Most Popular</h2>
684
+ <PluginList plugins={popular} />
685
+ </section>
686
+
687
+ {/* Recent */}
688
+ <section className="py-8">
689
+ <h2 className="text-2xl font-semibold mb-4">Recently Updated</h2>
690
+ <PluginList plugins={recent} />
691
+ </section>
692
+ </div>
693
+ );
694
+ }
695
+ ```
696
+
697
+ ### Plugin Card Component
698
+
699
+ ```typescript
700
+ // components/PluginCard.tsx
701
+
702
+ interface PluginCardProps {
703
+ plugin: PluginSummary;
704
+ featured?: boolean;
705
+ }
706
+
707
+ export function PluginCard({ plugin, featured }: PluginCardProps) {
708
+ return (
709
+ <div className={`border rounded-lg p-4 hover:shadow-lg transition-shadow ${
710
+ featured ? "border-blue-500" : "border-gray-200"
711
+ }`}>
712
+ <div className="flex items-start justify-between">
713
+ <div>
714
+ <h3 className="text-lg font-semibold">
715
+ <Link href={`/plugins/${plugin.name}`}>
716
+ {plugin.displayName || plugin.name}
717
+ </Link>
718
+ {plugin.verified && (
719
+ <span className="ml-2 text-blue-500" title="Verified">✓</span>
720
+ )}
721
+ </h3>
722
+ <p className="text-sm text-gray-500">{plugin.author}</p>
723
+ </div>
724
+ <span className="text-sm text-gray-400">{plugin.latestVersion}</span>
725
+ </div>
726
+
727
+ <p className="mt-2 text-gray-700 line-clamp-2">{plugin.description}</p>
728
+
729
+ <div className="mt-4 flex items-center gap-4 text-sm text-gray-500">
730
+ <span>⭐ {plugin.rating.toFixed(1)}</span>
731
+ <span>⬇️ {formatNumber(plugin.downloads)}</span>
732
+ </div>
733
+
734
+ <div className="mt-3 flex flex-wrap gap-1">
735
+ {plugin.categories.map((cat) => (
736
+ <Badge key={cat} variant="secondary">{cat}</Badge>
737
+ ))}
738
+ </div>
739
+
740
+ <div className="mt-4 pt-4 border-t flex justify-between items-center">
741
+ <code className="text-sm bg-gray-100 px-2 py-1 rounded">
742
+ donkeylabs plugin install {plugin.name}
743
+ </code>
744
+ <Button size="sm">View Details</Button>
745
+ </div>
746
+ </div>
747
+ );
748
+ }
749
+ ```
750
+
751
+ ---
752
+
753
+ ## Versioning & Dependencies
754
+
755
+ ### Semantic Versioning
756
+
757
+ Plugins follow semver: `MAJOR.MINOR.PATCH`
758
+
759
+ - **MAJOR**: Breaking changes (require migration)
760
+ - **MINOR**: New features (backward compatible)
761
+ - **PATCH**: Bug fixes (backward compatible)
762
+
763
+ ### Dependency Resolution
764
+
765
+ ```typescript
766
+ // packages/cli/src/registry/dependencies.ts
767
+
768
+ class DependencyResolver {
769
+ async resolve(pluginName: string, versionRange: string): Promise<ResolvedDependency[]> {
770
+ const plugin = await this.registry.getPlugin(pluginName);
771
+ const resolved: ResolvedDependency[] = [];
772
+
773
+ // Find best matching version
774
+ const version = this.findBestVersion(plugin.versions, versionRange);
775
+ if (!version) {
776
+ throw new Error(`No version found for ${pluginName}@${versionRange}`);
777
+ }
778
+
779
+ resolved.push({ name: pluginName, version: version.version });
780
+
781
+ // Recursively resolve plugin dependencies
782
+ if (version.dependencies?.plugins) {
783
+ for (const dep of version.dependencies.plugins) {
784
+ const [depName, depRange] = dep.split("@");
785
+ const depResolved = await this.resolve(depName, depRange || "*");
786
+ resolved.push(...depResolved);
787
+ }
788
+ }
789
+
790
+ // Check for conflicts
791
+ this.checkConflicts(resolved);
792
+
793
+ return resolved;
794
+ }
795
+
796
+ private findBestVersion(versions: PluginVersion[], range: string): PluginVersion | null {
797
+ // Sort by version descending
798
+ const sorted = versions
799
+ .filter(v => !v.deprecated)
800
+ .sort((a, b) => semver.rcompare(a.version, b.version));
801
+
802
+ // Find first matching version
803
+ return sorted.find(v => semver.satisfies(v.version, range)) || null;
804
+ }
805
+
806
+ private checkConflicts(resolved: ResolvedDependency[]) {
807
+ const versions = new Map<string, string[]>();
808
+
809
+ for (const dep of resolved) {
810
+ const existing = versions.get(dep.name) || [];
811
+ if (existing.length > 0 && !existing.includes(dep.version)) {
812
+ throw new Error(
813
+ `Version conflict for ${dep.name}: ${existing.join(", ")} vs ${dep.version}`
814
+ );
815
+ }
816
+ versions.set(dep.name, [...existing, dep.version]);
817
+ }
818
+ }
819
+ }
820
+ ```
821
+
822
+ ### Lockfile
823
+
824
+ Similar to `package-lock.json` or `bun.lockb`:
825
+
826
+ ```json
827
+ // donkeylabs-lock.json
828
+ {
829
+ "lockfileVersion": 1,
830
+ "plugins": {
831
+ "auth-jwt": {
832
+ "version": "2.1.0",
833
+ "resolved": "https://registry.donkeylabs.io/api/plugins/auth-jwt/2.1.0/download",
834
+ "integrity": "sha256:abc123...",
835
+ "dependencies": {
836
+ "users": "^1.0.0"
837
+ }
838
+ },
839
+ "users": {
840
+ "version": "1.2.0",
841
+ "resolved": "https://registry.donkeylabs.io/api/plugins/users/1.2.0/download",
842
+ "integrity": "sha256:def456...",
843
+ "dependencies": {}
844
+ }
845
+ }
846
+ }
847
+ ```
848
+
849
+ ---
850
+
851
+ ## Security & Validation
852
+
853
+ ### Plugin Validation
854
+
855
+ ```typescript
856
+ // packages/cli/src/registry/validation.ts
857
+
858
+ class PluginValidator {
859
+ async validate(pluginPath: string): Promise<ValidationResult> {
860
+ const errors: string[] = [];
861
+ const warnings: string[] = [];
862
+
863
+ // 1. Check structure
864
+ const requiredFiles = ["index.ts", "package.json"];
865
+ for (const file of requiredFiles) {
866
+ if (!await this.fileExists(join(pluginPath, file))) {
867
+ errors.push(`Missing required file: ${file}`);
868
+ }
869
+ }
870
+
871
+ // 2. Parse package.json
872
+ const pkg = await this.readPackageJson(pluginPath);
873
+
874
+ // 3. Validate plugin definition
875
+ const pluginDef = await this.parsePluginDefinition(pluginPath);
876
+ if (!pluginDef.name) errors.push("Plugin name not defined");
877
+ if (!pluginDef.service) errors.push("Plugin service not defined");
878
+
879
+ // 4. Check for security issues
880
+ const securityScan = await this.scanForSecurityIssues(pluginPath);
881
+ errors.push(...securityScan.errors);
882
+ warnings.push(...securityScan.warnings);
883
+
884
+ // 5. Check TypeScript compilation
885
+ const compileCheck = await this.checkTypescript(pluginPath);
886
+ if (!compileCheck.success) {
887
+ errors.push(`TypeScript errors: ${compileCheck.errors.join(", ")}`);
888
+ }
889
+
890
+ // 6. Run tests if available
891
+ if (await this.hasTests(pluginPath)) {
892
+ const testResult = await this.runTests(pluginPath);
893
+ if (!testResult.success) {
894
+ warnings.push("Tests failed (not blocking)");
895
+ }
896
+ }
897
+
898
+ return {
899
+ valid: errors.length === 0,
900
+ name: pluginDef.name,
901
+ version: pkg.version,
902
+ errors,
903
+ warnings,
904
+ };
905
+ }
906
+
907
+ private async scanForSecurityIssues(pluginPath: string): Promise<SecurityScan> {
908
+ const issues: string[] = [];
909
+ const warnings: string[] = [];
910
+
911
+ // Check for dangerous patterns
912
+ const code = await this.readAllSourceFiles(pluginPath);
913
+
914
+ // SQL injection risks
915
+ if (code.includes("sql`") && code.includes("${")) {
916
+ issues.push("Potential SQL injection: template literal in SQL query");
917
+ }
918
+
919
+ // Hardcoded secrets
920
+ const secretPatterns = [/password\s*[:=]\s*["'][^"']+["']/i, /secret\s*[:=]\s*["'][^"']+["']/i];
921
+ for (const pattern of secretPatterns) {
922
+ if (pattern.test(code)) {
923
+ warnings.push("Possible hardcoded secrets detected");
924
+ break;
925
+ }
926
+ }
927
+
928
+ // Unsafe eval
929
+ if (code.includes("eval(") || code.includes("new Function(")) {
930
+ issues.push("Unsafe eval() or new Function() detected");
931
+ }
932
+
933
+ return { errors: issues, warnings };
934
+ }
935
+ }
936
+ ```
937
+
938
+ ### Package Signing
939
+
940
+ ```typescript
941
+ // Sign packages for integrity verification
942
+ import { createSign } from "crypto";
943
+
944
+ class PackageSigner {
945
+ async sign(packageBuffer: Buffer, privateKey: string): Promise<string> {
946
+ const signer = createSign("SHA256");
947
+ signer.update(packageBuffer);
948
+ signer.end();
949
+ return signer.sign(privateKey, "base64");
950
+ }
951
+
952
+ async verify(packageBuffer: Buffer, signature: string, publicKey: string): Promise<boolean> {
953
+ const verifier = createVerify("SHA256");
954
+ verifier.update(packageBuffer);
955
+ verifier.end();
956
+ return verifier.verify(publicKey, signature, "base64");
957
+ }
958
+ }
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Implementation Roadmap
964
+
965
+ ### Phase 1: MVP (4-6 weeks)
966
+
967
+ **Week 1-2: Backend**
968
+ - [ ] Registry API server setup
969
+ - [ ] Database schema
970
+ - [ ] Package storage (S3/MinIO)
971
+ - [ ] Basic CRUD endpoints
972
+
973
+ **Week 3: CLI Integration**
974
+ - [ ] `donkeylabs plugin search`
975
+ - [ ] `donkeylabs plugin install`
976
+ - [ ] `donkeylabs plugin uninstall`
977
+ - [ ] Local plugin registry cache
978
+
979
+ **Week 4: Discovery**
980
+ - [ ] Web UI (search + browse)
981
+ - [ ] Plugin detail pages
982
+ - [ ] Basic rating system
983
+
984
+ **Week 5-6: Polish**
985
+ - [ ] Authentication (GitHub OAuth)
986
+ - [ ] Plugin validation
987
+ - [ ] Documentation
988
+
989
+ ### Phase 2: Enhanced (6-8 weeks)
990
+
991
+ - [ ] Semantic versioning support
992
+ - [ ] Dependency resolution
993
+ - [ ] Lockfile management
994
+ - [ ] Update notifications
995
+ - [ ] Verified plugin badges
996
+ - [ ] Plugin statistics dashboard
997
+
998
+ ### Phase 3: Ecosystem (8+ weeks)
999
+
1000
+ - [ ] Plugin templates/scaffolding
1001
+ - [ ] Automated testing on publish
1002
+ - [ ] Plugin marketplace (paid plugins)
1003
+ - [ ] Plugin analytics for authors
1004
+ - [ ] Community features (comments, forums)
1005
+
1006
+ ### Directory Structure
1007
+
1008
+ ```
1009
+ packages/
1010
+ ├── registry/ # Registry backend
1011
+ │ ├── src/
1012
+ │ │ ├── server.ts # API server
1013
+ │ │ ├── routes/ # API routes
1014
+ │ │ ├── plugins/ # Registry plugins
1015
+ │ │ │ ├── search.ts
1016
+ │ │ │ ├── storage.ts
1017
+ │ │ │ └── auth.ts
1018
+ │ │ └── services/
1019
+ │ ├── web/ # Next.js frontend
1020
+ │ │ ├── app/
1021
+ │ │ ├── components/
1022
+ │ │ └── lib/
1023
+ │ └── package.json
1024
+
1025
+ ├── cli/src/
1026
+ │ └── commands/
1027
+ │ └── registry.ts # CLI commands
1028
+
1029
+ └── plugin-sdk/ # SDK for plugin dev
1030
+ ├── src/
1031
+ │ ├── validation.ts
1032
+ │ ├── testing.ts
1033
+ │ └── publishing.ts
1034
+ └── package.json
1035
+ ```
1036
+
1037
+ ### Hosting
1038
+
1039
+ **Recommended stack:**
1040
+ - **API**: Vercel/Netlify Functions or Railway
1041
+ - **Database**: PostgreSQL (Supabase or Railway)
1042
+ - **Storage**: AWS S3 or Cloudflare R2
1043
+ - **Search**: Algolia (free tier sufficient initially)
1044
+ - **CDN**: Cloudflare
1045
+ - **Auth**: GitHub OAuth + JWT
1046
+
1047
+ ---
1048
+
1049
+ ## Summary
1050
+
1051
+ This registry system will:
1052
+
1053
+ 1. **Enable discovery** - Search and browse 1000s of plugins
1054
+ 2. **Simplify sharing** - One command to publish
1055
+ 3. **Ensure quality** - Validation + testing
1056
+ 4. **Manage versions** - Semantic versioning + lockfiles
1057
+ 5. **Foster community** - Ratings, reviews, contributions
1058
+
1059
+ **Next steps:**
1060
+ 1. Set up registry server infrastructure
1061
+ 2. Implement core CLI commands
1062
+ 3. Build web UI
1063
+ 4. Launch beta with curated plugins
1064
+ 5. Open to community submissions