@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.
- package/docs/caching-strategies.md +677 -0
- package/docs/dev-experience.md +656 -0
- package/docs/hot-reload-limitations.md +166 -0
- package/docs/load-testing.md +974 -0
- package/docs/plugin-registry-design.md +1064 -0
- package/docs/production.md +1229 -0
- package/docs/workflows.md +90 -3
- package/package.json +1 -1
- package/src/admin/routes.ts +153 -0
- package/src/core/cron.ts +90 -9
- package/src/core/index.ts +31 -0
- package/src/core/job-adapter-kysely.ts +176 -73
- package/src/core/job-adapter-sqlite.ts +10 -0
- package/src/core/jobs.ts +112 -17
- package/src/core/migrations/workflows/002_add_metadata_column.ts +28 -0
- package/src/core/process-adapter-kysely.ts +62 -21
- package/src/core/storage-adapter-local.test.ts +199 -0
- package/src/core/storage.test.ts +197 -0
- package/src/core/workflow-adapter-kysely.ts +66 -19
- package/src/core/workflow-executor.ts +239 -0
- package/src/core/workflow-proxy.ts +238 -0
- package/src/core/workflow-socket.ts +449 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +758 -0
- package/src/core/workflows.ts +705 -595
- package/src/core.ts +17 -6
- package/src/index.ts +14 -0
- package/src/testing/database.test.ts +263 -0
- package/src/testing/database.ts +173 -0
- package/src/testing/e2e.test.ts +189 -0
- package/src/testing/e2e.ts +272 -0
- package/src/testing/index.ts +18 -0
|
@@ -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
|