@donkeylabs/cli 0.1.0

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/package.json +51 -0
  4. package/src/commands/generate.ts +585 -0
  5. package/src/commands/init.ts +201 -0
  6. package/src/commands/interactive.ts +223 -0
  7. package/src/commands/plugin.ts +205 -0
  8. package/src/index.ts +108 -0
  9. package/templates/starter/.env.example +3 -0
  10. package/templates/starter/.gitignore.template +4 -0
  11. package/templates/starter/CLAUDE.md +144 -0
  12. package/templates/starter/donkeylabs.config.ts +6 -0
  13. package/templates/starter/package.json +21 -0
  14. package/templates/starter/src/client.test.ts +7 -0
  15. package/templates/starter/src/db.ts +9 -0
  16. package/templates/starter/src/index.ts +48 -0
  17. package/templates/starter/src/plugins/stats/index.ts +105 -0
  18. package/templates/starter/src/routes/health/index.ts +5 -0
  19. package/templates/starter/src/routes/health/ping/index.ts +13 -0
  20. package/templates/starter/src/routes/health/ping/models/model.ts +23 -0
  21. package/templates/starter/src/routes/health/ping/schema.ts +14 -0
  22. package/templates/starter/src/routes/health/ping/tests/integ.test.ts +20 -0
  23. package/templates/starter/src/routes/health/ping/tests/unit.test.ts +21 -0
  24. package/templates/starter/src/test-ctx.ts +24 -0
  25. package/templates/starter/tsconfig.json +27 -0
  26. package/templates/sveltekit-app/.env.example +3 -0
  27. package/templates/sveltekit-app/README.md +103 -0
  28. package/templates/sveltekit-app/donkeylabs.config.ts +10 -0
  29. package/templates/sveltekit-app/package.json +36 -0
  30. package/templates/sveltekit-app/src/app.css +40 -0
  31. package/templates/sveltekit-app/src/app.html +12 -0
  32. package/templates/sveltekit-app/src/hooks.server.ts +4 -0
  33. package/templates/sveltekit-app/src/lib/api.ts +134 -0
  34. package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +30 -0
  35. package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +3 -0
  36. package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +48 -0
  37. package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +9 -0
  38. package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +18 -0
  39. package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +18 -0
  40. package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +18 -0
  41. package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +18 -0
  42. package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +18 -0
  43. package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +21 -0
  44. package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +21 -0
  45. package/templates/sveltekit-app/src/lib/components/ui/index.ts +4 -0
  46. package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +2 -0
  47. package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +20 -0
  48. package/templates/sveltekit-app/src/lib/utils/index.ts +6 -0
  49. package/templates/sveltekit-app/src/routes/+layout.svelte +8 -0
  50. package/templates/sveltekit-app/src/routes/+page.server.ts +25 -0
  51. package/templates/sveltekit-app/src/routes/+page.svelte +401 -0
  52. package/templates/sveltekit-app/src/server/index.ts +263 -0
  53. package/templates/sveltekit-app/static/robots.txt +3 -0
  54. package/templates/sveltekit-app/svelte.config.js +18 -0
  55. package/templates/sveltekit-app/tsconfig.json +25 -0
  56. package/templates/sveltekit-app/vite.config.ts +7 -0
@@ -0,0 +1,401 @@
1
+ <script lang="ts">
2
+ import { browser } from '$app/environment';
3
+ import { onMount } from 'svelte';
4
+ import { Button } from '$lib/components/ui/button';
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
6
+ import { Input } from '$lib/components/ui/input';
7
+ import { Badge } from '$lib/components/ui/badge';
8
+ import { createApi, type CronTask } from '$lib/api';
9
+
10
+ let { data } = $props();
11
+
12
+ // Create typed API client (browser mode - no locals)
13
+ const api = createApi();
14
+
15
+ // SSE Events state
16
+ let events = $state<Array<{ id: number; message: string; timestamp: string; source?: string }>>([]);
17
+ let sseConnected = $state(false);
18
+ let sseClients = $state({ total: 0, byChannel: 0 });
19
+
20
+ // Counter state
21
+ let count = $state(data.count);
22
+ let counterLoading = $state(false);
23
+
24
+ // Cache state
25
+ let cacheKey = $state('demo-key');
26
+ let cacheValue = $state('Hello World');
27
+ let cacheTTL = $state(30000);
28
+ let cacheResult = $state<any>(null);
29
+ let cacheKeys = $state<string[]>([]);
30
+
31
+ // Jobs state
32
+ let jobMessage = $state('Test job');
33
+ let jobDelay = $state(0);
34
+ let jobStats = $state({ pending: 0, running: 0, completed: 0 });
35
+ let lastJobId = $state<string | null>(null);
36
+
37
+ // Rate Limiter state
38
+ let rateLimitKey = $state('demo');
39
+ let rateLimitMax = $state(5);
40
+ let rateLimitWindow = $state(60000);
41
+ let rateLimitResult = $state<any>(null);
42
+
43
+ // Cron state
44
+ let cronTasks = $state<CronTask[]>([]);
45
+
46
+ // Events (pub/sub) state
47
+ let eventName = $state('demo.test');
48
+ let eventData = $state('{"hello": "world"}');
49
+
50
+ // Counter actions - using typed client
51
+ async function counterAction(action: 'get' | 'increment' | 'decrement' | 'reset') {
52
+ counterLoading = true;
53
+ const result = await api.counter[action]();
54
+ count = result.count;
55
+ counterLoading = false;
56
+ }
57
+
58
+ // Cache actions - using typed client
59
+ async function cacheSet() {
60
+ await api.cache.set({ key: cacheKey, value: cacheValue, ttl: cacheTTL });
61
+ cacheResult = { action: 'set', success: true };
62
+ refreshCacheKeys();
63
+ }
64
+
65
+ async function cacheGet() {
66
+ cacheResult = await api.cache.get({ key: cacheKey });
67
+ refreshCacheKeys();
68
+ }
69
+
70
+ async function cacheDelete() {
71
+ await api.cache.delete({ key: cacheKey });
72
+ cacheResult = { action: 'deleted', success: true };
73
+ refreshCacheKeys();
74
+ }
75
+
76
+ async function refreshCacheKeys() {
77
+ const result = await api.cache.keys();
78
+ cacheKeys = result.keys || [];
79
+ }
80
+
81
+ // Jobs actions - using typed client
82
+ async function enqueueJob() {
83
+ const result = await api.jobs.enqueue({
84
+ name: 'demo-job',
85
+ data: { message: jobMessage },
86
+ delay: jobDelay > 0 ? jobDelay : undefined
87
+ });
88
+ lastJobId = result.jobId;
89
+ refreshJobStats();
90
+ }
91
+
92
+ async function refreshJobStats() {
93
+ jobStats = await api.jobs.stats();
94
+ }
95
+
96
+ // Rate limiter actions - using typed client
97
+ async function checkRateLimit() {
98
+ rateLimitResult = await api.ratelimit.check({
99
+ key: rateLimitKey,
100
+ limit: rateLimitMax,
101
+ window: rateLimitWindow
102
+ });
103
+ }
104
+
105
+ async function resetRateLimit() {
106
+ await api.ratelimit.reset({ key: rateLimitKey });
107
+ rateLimitResult = { reset: true, message: 'Rate limit reset' };
108
+ }
109
+
110
+ // Cron actions - using typed client
111
+ async function refreshCronTasks() {
112
+ const result = await api.cron.list();
113
+ cronTasks = result.tasks;
114
+ }
115
+
116
+ // Events (pub/sub) actions - using typed client
117
+ async function emitEvent() {
118
+ try {
119
+ const parsedData = JSON.parse(eventData);
120
+ await api.events.emit({ event: eventName, data: parsedData });
121
+ } catch (e) {
122
+ console.error('Invalid JSON:', e);
123
+ }
124
+ }
125
+
126
+ // SSE actions - using typed client
127
+ async function manualBroadcast() {
128
+ await api.sseRoutes.broadcast({
129
+ channel: 'events',
130
+ event: 'manual',
131
+ data: {
132
+ id: Date.now(),
133
+ message: 'Manual broadcast!',
134
+ timestamp: new Date().toISOString(),
135
+ source: 'manual'
136
+ }
137
+ });
138
+ }
139
+
140
+ async function refreshSSEClients() {
141
+ sseClients = await api.sseRoutes.clients();
142
+ }
143
+
144
+ onMount(() => {
145
+ if (!browser) return;
146
+
147
+ // Initial data fetches
148
+ refreshCacheKeys();
149
+ refreshJobStats();
150
+ refreshCronTasks();
151
+ refreshSSEClients();
152
+
153
+ // SSE subscription using the typed client
154
+ const unsubscribe = api.sse.subscribe(['events'], (eventType, eventData) => {
155
+ // Handle all event types
156
+ if (['cron-event', 'job-completed', 'internal-event', 'manual'].includes(eventType)) {
157
+ // Simple prepend - CSS handles animation via :first-child or key-based animation
158
+ events = [{ ...eventData }, ...events].slice(0, 15);
159
+
160
+ if (eventType === 'job-completed') {
161
+ refreshJobStats();
162
+ }
163
+ }
164
+ });
165
+
166
+ // Track connection status
167
+ const checkConnection = setInterval(() => {
168
+ // The SSE subscribe auto-reconnects, so we just refresh clients
169
+ refreshSSEClients().then(() => {
170
+ sseConnected = sseClients.byChannel > 0;
171
+ });
172
+ refreshJobStats();
173
+ }, 5000);
174
+
175
+ // Set connected after initial subscribe
176
+ setTimeout(() => {
177
+ sseConnected = true;
178
+ refreshSSEClients();
179
+ }, 500);
180
+
181
+ return () => {
182
+ unsubscribe();
183
+ clearInterval(checkConnection);
184
+ };
185
+ });
186
+
187
+ function getSourceColor(source?: string): "default" | "secondary" | "destructive" | "outline" | "success" {
188
+ switch (source) {
189
+ case 'cron': return 'default';
190
+ case 'manual': return 'secondary';
191
+ case 'events': return 'outline';
192
+ default: return 'success';
193
+ }
194
+ }
195
+
196
+ function getSourceLabel(source?: string) {
197
+ switch (source) {
198
+ case 'cron': return 'CRON';
199
+ case 'manual': return 'MANUAL';
200
+ case 'events': return 'PUB/SUB';
201
+ default: return 'JOB';
202
+ }
203
+ }
204
+ </script>
205
+
206
+ <div class="min-h-screen bg-background">
207
+ <div class="container mx-auto max-w-7xl py-8 px-4">
208
+ <!-- Header -->
209
+ <div class="text-center mb-8">
210
+ <h1 class="text-3xl font-bold tracking-tight">@donkeylabs/server Demo</h1>
211
+ <p class="text-muted-foreground mt-2">SvelteKit Adapter — All Core Services</p>
212
+ <Badge variant="outline" class="mt-3">
213
+ SSR: {data.isSSR ? 'Yes' : 'No'} | Loaded: {data.loadedAt}
214
+ </Badge>
215
+ </div>
216
+
217
+ <!-- Grid of feature cards -->
218
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
219
+ <!-- Counter / RPC Demo -->
220
+ <Card>
221
+ <CardHeader>
222
+ <CardTitle>RPC Routes</CardTitle>
223
+ <CardDescription>Type-safe API calls with Zod validation</CardDescription>
224
+ </CardHeader>
225
+ <CardContent>
226
+ <div class="text-center py-4">
227
+ <span class="text-5xl font-bold text-primary">{count}</span>
228
+ </div>
229
+ <div class="flex gap-2 justify-center">
230
+ <Button variant="outline" size="icon" onclick={() => counterAction('decrement')} disabled={counterLoading}>−</Button>
231
+ <Button variant="secondary" onclick={() => counterAction('get')} disabled={counterLoading}>Refresh</Button>
232
+ <Button variant="outline" size="icon" onclick={() => counterAction('increment')} disabled={counterLoading}>+</Button>
233
+ <Button variant="ghost" onclick={() => counterAction('reset')} disabled={counterLoading}>Reset</Button>
234
+ </div>
235
+ </CardContent>
236
+ </Card>
237
+
238
+ <!-- Cache Demo -->
239
+ <Card>
240
+ <CardHeader>
241
+ <CardTitle>Cache</CardTitle>
242
+ <CardDescription>In-memory caching with TTL support</CardDescription>
243
+ </CardHeader>
244
+ <CardContent class="space-y-3">
245
+ <div class="flex gap-2">
246
+ <Input bind:value={cacheKey} placeholder="Key" class="flex-1" />
247
+ <Input bind:value={cacheValue} placeholder="Value" class="flex-1" />
248
+ </div>
249
+ <div class="flex gap-2">
250
+ <Button onclick={cacheSet} size="sm">Set</Button>
251
+ <Button onclick={cacheGet} size="sm" variant="secondary">Get</Button>
252
+ <Button onclick={cacheDelete} size="sm" variant="outline">Delete</Button>
253
+ </div>
254
+ {#if cacheResult}
255
+ <pre class="text-xs bg-muted p-2 rounded-md overflow-auto">{JSON.stringify(cacheResult, null, 2)}</pre>
256
+ {/if}
257
+ <p class="text-xs text-muted-foreground">
258
+ Keys ({cacheKeys.length}): {cacheKeys.length > 0 ? cacheKeys.join(', ') : 'none'}
259
+ </p>
260
+ </CardContent>
261
+ </Card>
262
+
263
+ <!-- Jobs Demo -->
264
+ <Card>
265
+ <CardHeader>
266
+ <CardTitle>Background Jobs</CardTitle>
267
+ <CardDescription>Async job queue with optional delay</CardDescription>
268
+ </CardHeader>
269
+ <CardContent class="space-y-3">
270
+ <div class="flex gap-2">
271
+ <Input bind:value={jobMessage} placeholder="Job message" class="flex-1" />
272
+ <Input bind:value={jobDelay} type="number" placeholder="Delay" class="w-20" />
273
+ </div>
274
+ <div class="flex gap-2">
275
+ <Button onclick={enqueueJob} size="sm">Enqueue</Button>
276
+ <Button onclick={refreshJobStats} size="sm" variant="outline">Refresh</Button>
277
+ </div>
278
+ {#if lastJobId}
279
+ <p class="text-xs text-muted-foreground">Last Job: <code class="bg-muted px-1 rounded">{lastJobId}</code></p>
280
+ {/if}
281
+ <div class="flex gap-3 text-xs text-muted-foreground">
282
+ <span>Pending: {jobStats.pending}</span>
283
+ <span>Running: {jobStats.running}</span>
284
+ <span>Done: {jobStats.completed}</span>
285
+ </div>
286
+ </CardContent>
287
+ </Card>
288
+
289
+ <!-- Rate Limiter Demo -->
290
+ <Card>
291
+ <CardHeader>
292
+ <CardTitle>Rate Limiter</CardTitle>
293
+ <CardDescription>Sliding window rate limiting</CardDescription>
294
+ </CardHeader>
295
+ <CardContent class="space-y-3">
296
+ <div class="flex gap-2">
297
+ <Input bind:value={rateLimitKey} placeholder="Key" class="flex-1" />
298
+ <Input bind:value={rateLimitMax} type="number" placeholder="Limit" class="w-16" />
299
+ <Input bind:value={rateLimitWindow} type="number" placeholder="Window" class="w-20" />
300
+ </div>
301
+ <div class="flex gap-2">
302
+ <Button onclick={checkRateLimit} size="sm">Check</Button>
303
+ <Button onclick={resetRateLimit} size="sm" variant="outline">Reset</Button>
304
+ </div>
305
+ {#if rateLimitResult}
306
+ <pre class="text-xs p-2 rounded-md overflow-auto {rateLimitResult.allowed === false ? 'bg-destructive/10 text-destructive' : 'bg-muted'}">{JSON.stringify(rateLimitResult, null, 2)}</pre>
307
+ {/if}
308
+ </CardContent>
309
+ </Card>
310
+
311
+ <!-- Cron Demo -->
312
+ <Card>
313
+ <CardHeader>
314
+ <CardTitle>Cron Jobs</CardTitle>
315
+ <CardDescription>Scheduled tasks with cron expressions</CardDescription>
316
+ </CardHeader>
317
+ <CardContent class="space-y-3">
318
+ <Button onclick={refreshCronTasks} size="sm" variant="outline">Refresh Tasks</Button>
319
+ {#if cronTasks.length > 0}
320
+ <ul class="space-y-2">
321
+ {#each cronTasks as task}
322
+ <li class="flex items-center gap-2 text-sm">
323
+ <span class="font-medium">{task.name}</span>
324
+ <code class="text-xs bg-muted px-1 rounded">{task.expression}</code>
325
+ <Badge variant={task.enabled ? 'success' : 'secondary'} class="text-xs">
326
+ {task.enabled ? 'Active' : 'Paused'}
327
+ </Badge>
328
+ </li>
329
+ {/each}
330
+ </ul>
331
+ {:else}
332
+ <p class="text-sm text-muted-foreground italic">No scheduled tasks</p>
333
+ {/if}
334
+ </CardContent>
335
+ </Card>
336
+
337
+ <!-- Events (Pub/Sub) Demo -->
338
+ <Card>
339
+ <CardHeader>
340
+ <CardTitle>Events (Pub/Sub)</CardTitle>
341
+ <CardDescription>Internal event system with wildcards</CardDescription>
342
+ </CardHeader>
343
+ <CardContent class="space-y-3">
344
+ <Input bind:value={eventName} placeholder="Event name (e.g., demo.test)" />
345
+ <textarea
346
+ bind:value={eventData}
347
+ placeholder="JSON data"
348
+ rows="2"
349
+ class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
350
+ ></textarea>
351
+ <Button onclick={emitEvent} size="sm">Emit Event</Button>
352
+ <p class="text-xs text-muted-foreground italic">Events matching "demo.*" broadcast to SSE</p>
353
+ </CardContent>
354
+ </Card>
355
+ </div>
356
+
357
+ <!-- SSE Events Stream - Full Width -->
358
+ <Card>
359
+ <CardHeader class="flex flex-row items-center justify-between">
360
+ <div>
361
+ <CardTitle>Live Events (SSE)</CardTitle>
362
+ <CardDescription>Real-time server → client push</CardDescription>
363
+ </div>
364
+ <div class="flex items-center gap-2">
365
+ <span class="relative flex h-3 w-3">
366
+ {#if sseConnected}
367
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
368
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
369
+ {:else}
370
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-gray-400"></span>
371
+ {/if}
372
+ </span>
373
+ <span class="text-sm text-muted-foreground">
374
+ {sseConnected ? 'Connected' : 'Disconnected'} ({sseClients.byChannel} clients)
375
+ </span>
376
+ </div>
377
+ </CardHeader>
378
+ <CardContent>
379
+ <div class="flex gap-2 mb-4">
380
+ <Button onclick={manualBroadcast} size="sm">Manual Broadcast</Button>
381
+ <Button onclick={refreshSSEClients} size="sm" variant="outline">Refresh Clients</Button>
382
+ </div>
383
+ {#if events.length === 0}
384
+ <p class="text-sm text-muted-foreground italic">Waiting for events... (cron broadcasts every 5s)</p>
385
+ {:else}
386
+ <ul class="space-y-2 max-h-80 overflow-y-auto">
387
+ {#each events as event}
388
+ <li
389
+ class="flex items-center gap-3 p-3 rounded-lg border bg-muted/50 animate-in slide-in-from-left-2 duration-300"
390
+ >
391
+ <Badge variant={getSourceColor(event.source)}>{getSourceLabel(event.source)}</Badge>
392
+ <span class="flex-1 text-sm font-medium">{event.message}</span>
393
+ <span class="text-xs text-muted-foreground">{new Date(event.timestamp).toLocaleTimeString()}</span>
394
+ </li>
395
+ {/each}
396
+ </ul>
397
+ {/if}
398
+ </CardContent>
399
+ </Card>
400
+ </div>
401
+ </div>
@@ -0,0 +1,263 @@
1
+ // Server entry for @donkeylabs/adapter-sveltekit
2
+ import { AppServer, createPlugin, createRouter } from "@donkeylabs/server";
3
+ import { Kysely } from "kysely";
4
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
5
+ import { Database } from "bun:sqlite";
6
+ import { z } from "zod";
7
+
8
+ // Simple in-memory database
9
+ const db = new Kysely<{}>({
10
+ dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
11
+ });
12
+
13
+ // Random event messages for SSE demo
14
+ const eventMessages = [
15
+ "User logged in",
16
+ "New order placed",
17
+ "Payment received",
18
+ "Item shipped",
19
+ "Review submitted",
20
+ "Comment added",
21
+ "File uploaded",
22
+ "Task completed",
23
+ "Alert triggered",
24
+ "Sync finished",
25
+ ];
26
+
27
+ // Demo plugin with all core service integrations
28
+ const demoPlugin = createPlugin.define({
29
+ name: "demo",
30
+ service: async (ctx) => {
31
+ let counter = 0;
32
+
33
+ return {
34
+ // Counter
35
+ getCounter: () => counter,
36
+ increment: () => ++counter,
37
+ decrement: () => --counter,
38
+ reset: () => { counter = 0; return counter; },
39
+
40
+ // Cache helpers
41
+ cacheSet: async (key: string, value: any, ttl?: number) => {
42
+ await ctx.core.cache.set(key, value, ttl);
43
+ return { success: true };
44
+ },
45
+ cacheGet: async (key: string) => {
46
+ const value = await ctx.core.cache.get(key);
47
+ const exists = await ctx.core.cache.has(key);
48
+ return { value, exists };
49
+ },
50
+ cacheDelete: async (key: string) => {
51
+ await ctx.core.cache.delete(key);
52
+ return { success: true };
53
+ },
54
+ cacheKeys: async () => {
55
+ const keys = await ctx.core.cache.keys();
56
+ return { keys, size: keys.length };
57
+ },
58
+
59
+ // Jobs helpers
60
+ enqueueJob: async (name: string, data: any, delay?: number) => {
61
+ let jobId: string;
62
+ if (delay && delay > 0) {
63
+ const runAt = new Date(Date.now() + delay);
64
+ jobId = await ctx.core.jobs.schedule(name, data, runAt);
65
+ } else {
66
+ jobId = await ctx.core.jobs.enqueue(name, data);
67
+ }
68
+ return { jobId };
69
+ },
70
+ getJobStats: async () => {
71
+ const pending = await ctx.core.jobs.getByName("demo-job", "pending");
72
+ const running = await ctx.core.jobs.getByName("demo-job", "running");
73
+ const completed = await ctx.core.jobs.getByName("demo-job", "completed");
74
+ return {
75
+ pending: pending.length,
76
+ running: running.length,
77
+ completed: completed.length,
78
+ };
79
+ },
80
+
81
+ // Cron helpers
82
+ getCronTasks: () => ctx.core.cron.list().map(t => ({
83
+ id: t.id,
84
+ name: t.name,
85
+ expression: t.expression,
86
+ enabled: t.enabled,
87
+ lastRun: t.lastRun?.toISOString(),
88
+ nextRun: t.nextRun?.toISOString(),
89
+ })),
90
+
91
+ // Rate limiter helpers
92
+ checkRateLimit: async (key: string, limit: number, window: number) => {
93
+ return ctx.core.rateLimiter.check(key, limit, window);
94
+ },
95
+ resetRateLimit: async (key: string) => {
96
+ await ctx.core.rateLimiter.reset(key);
97
+ return { success: true };
98
+ },
99
+
100
+ // Events helpers (internal pub/sub)
101
+ emitEvent: async (event: string, data: any) => {
102
+ await ctx.core.events.emit(event, data);
103
+ return { success: true };
104
+ },
105
+
106
+ // SSE broadcast
107
+ broadcast: (channel: string, event: string, data: any) => {
108
+ ctx.core.sse.broadcast(channel, event, data);
109
+ return { success: true };
110
+ },
111
+ getSSEClients: () => ({
112
+ total: ctx.core.sse.getClients().length,
113
+ byChannel: ctx.core.sse.getClientsByChannel("events").length,
114
+ }),
115
+ };
116
+ },
117
+ init: async (ctx) => {
118
+ // Register job handler for demo
119
+ ctx.core.jobs.register("demo-job", async (data) => {
120
+ ctx.core.logger.info("Demo job executed", { data });
121
+ // Broadcast job completion via SSE
122
+ ctx.core.sse.broadcast("events", "job-completed", {
123
+ id: Date.now(),
124
+ message: `Job completed: ${data.message || "No message"}`,
125
+ timestamp: new Date().toISOString(),
126
+ });
127
+ });
128
+
129
+ // Schedule cron job to broadcast SSE events every 5 seconds
130
+ ctx.core.cron.schedule("*/5 * * * * *", () => {
131
+ const message = eventMessages[Math.floor(Math.random() * eventMessages.length)];
132
+ ctx.core.sse.broadcast("events", "cron-event", {
133
+ id: Date.now(),
134
+ message,
135
+ timestamp: new Date().toISOString(),
136
+ source: "cron",
137
+ });
138
+ }, { name: "sse-broadcaster" });
139
+
140
+ // Listen for internal events and broadcast to SSE
141
+ ctx.core.events.on("demo.*", (data) => {
142
+ ctx.core.sse.broadcast("events", "internal-event", {
143
+ id: Date.now(),
144
+ message: `Internal event: ${JSON.stringify(data)}`,
145
+ timestamp: new Date().toISOString(),
146
+ source: "events",
147
+ });
148
+ });
149
+
150
+ ctx.core.logger.info("Demo plugin initialized with all core services");
151
+ },
152
+ });
153
+
154
+ // Create routes
155
+ const api = createRouter("api");
156
+
157
+ // Counter routes
158
+ api.route("counter.get").typed({
159
+ handle: async (_input, ctx) => ({ count: ctx.plugins.demo.getCounter() }),
160
+ });
161
+
162
+ api.route("counter.increment").typed({
163
+ handle: async (_input, ctx) => ({ count: ctx.plugins.demo.increment() }),
164
+ });
165
+
166
+ api.route("counter.decrement").typed({
167
+ handle: async (_input, ctx) => ({ count: ctx.plugins.demo.decrement() }),
168
+ });
169
+
170
+ api.route("counter.reset").typed({
171
+ handle: async (_input, ctx) => ({ count: ctx.plugins.demo.reset() }),
172
+ });
173
+
174
+ // Cache routes
175
+ api.route("cache.set").typed({
176
+ input: z.object({
177
+ key: z.string(),
178
+ value: z.any(),
179
+ ttl: z.number().optional()
180
+ }),
181
+ handle: async (input, ctx) => ctx.plugins.demo.cacheSet(input.key, input.value, input.ttl),
182
+ });
183
+
184
+ api.route("cache.get").typed({
185
+ input: z.object({ key: z.string() }),
186
+ handle: async (input, ctx) => ctx.plugins.demo.cacheGet(input.key),
187
+ });
188
+
189
+ api.route("cache.delete").typed({
190
+ input: z.object({ key: z.string() }),
191
+ handle: async (input, ctx) => ctx.plugins.demo.cacheDelete(input.key),
192
+ });
193
+
194
+ api.route("cache.keys").typed({
195
+ handle: async (_input, ctx) => ctx.plugins.demo.cacheKeys(),
196
+ });
197
+
198
+ // Jobs routes
199
+ api.route("jobs.enqueue").typed({
200
+ input: z.object({
201
+ name: z.string().default("demo-job"),
202
+ data: z.any().default({}),
203
+ delay: z.number().optional()
204
+ }),
205
+ handle: async (input, ctx) => ctx.plugins.demo.enqueueJob(input.name, input.data, input.delay),
206
+ });
207
+
208
+ api.route("jobs.stats").typed({
209
+ handle: async (_input, ctx) => ctx.plugins.demo.getJobStats(),
210
+ });
211
+
212
+ // Cron routes
213
+ api.route("cron.list").typed({
214
+ handle: async (_input, ctx) => ({ tasks: ctx.plugins.demo.getCronTasks() }),
215
+ });
216
+
217
+ // Rate limiter routes
218
+ api.route("ratelimit.check").typed({
219
+ input: z.object({
220
+ key: z.string().default("demo"),
221
+ limit: z.number().default(5),
222
+ window: z.number().default(60000)
223
+ }),
224
+ handle: async (input, ctx) => ctx.plugins.demo.checkRateLimit(input.key, input.limit, input.window),
225
+ });
226
+
227
+ api.route("ratelimit.reset").typed({
228
+ input: z.object({ key: z.string().default("demo") }),
229
+ handle: async (input, ctx) => ctx.plugins.demo.resetRateLimit(input.key),
230
+ });
231
+
232
+ // Events routes (internal pub/sub)
233
+ api.route("events.emit").typed({
234
+ input: z.object({
235
+ event: z.string().default("demo.test"),
236
+ data: z.any().default({ test: true })
237
+ }),
238
+ handle: async (input, ctx) => ctx.plugins.demo.emitEvent(input.event, input.data),
239
+ });
240
+
241
+ // SSE routes
242
+ api.route("sse.broadcast").typed({
243
+ input: z.object({
244
+ channel: z.string().default("events"),
245
+ event: z.string().default("manual"),
246
+ data: z.any()
247
+ }),
248
+ handle: async (input, ctx) => ctx.plugins.demo.broadcast(input.channel, input.event, input.data),
249
+ });
250
+
251
+ api.route("sse.clients").typed({
252
+ handle: async (_input, ctx) => ctx.plugins.demo.getSSEClients(),
253
+ });
254
+
255
+ // Create server
256
+ export const server = new AppServer({
257
+ db,
258
+ port: 0, // Port managed by adapter
259
+ });
260
+
261
+ // Register plugin and routes
262
+ server.registerPlugin(demoPlugin);
263
+ server.use(api);
@@ -0,0 +1,3 @@
1
+ # allow crawling everything by default
2
+ User-agent: *
3
+ Disallow:
@@ -0,0 +1,18 @@
1
+ import adapter from '@donkeylabs/adapter-sveltekit';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ preprocess: vitePreprocess(),
7
+
8
+ kit: {
9
+ adapter: adapter({
10
+ serverEntry: './src/server/index.ts',
11
+ }),
12
+ alias: {
13
+ $server: '.@donkeylabs/server',
14
+ }
15
+ }
16
+ };
17
+
18
+ export default config;