@donkeylabs/cli 0.5.0 → 0.6.3

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,522 @@
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 {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "$lib/components/ui/card";
12
+ import { Badge } from "$lib/components/ui/badge";
13
+ import { createApi } from "$lib/api";
14
+
15
+ // Workflow steps for visual display
16
+ const WORKFLOW_STEPS = [
17
+ { id: "validate", label: "Validate Order", icon: "1" },
18
+ { id: "payment", label: "Process Payment", icon: "2" },
19
+ { id: "fulfill", label: "Fulfill Order", icon: "3", parallel: true },
20
+ { id: "complete", label: "Complete", icon: "4" },
21
+ ];
22
+
23
+ // Parallel sub-steps
24
+ const PARALLEL_STEPS = {
25
+ fulfill: [
26
+ { id: "send-email", label: "Send Email" },
27
+ { id: "prepare-shipment", label: "Prepare Shipment" },
28
+ ],
29
+ };
30
+
31
+ let { data } = $props();
32
+
33
+ const client = createApi();
34
+
35
+ // Active workflow state
36
+ let activeWorkflow = $state<any>(null);
37
+ let workflowProgress = $state(0);
38
+ let currentStep = $state<string | null>(null);
39
+ let stepStatuses = $state<Record<string, string>>({});
40
+ let stepResults = $state<Record<string, any>>({});
41
+ let isStarting = $state(false);
42
+
43
+ // History state
44
+ let instances = $state(data.instances || []);
45
+
46
+ // Event log for debugging
47
+ let eventLog = $state<Array<{ time: string; event: string; data: any }>>([]);
48
+
49
+ function getStepStatus(stepId: string): "pending" | "running" | "completed" | "failed" {
50
+ return (stepStatuses[stepId] as any) || "pending";
51
+ }
52
+
53
+ function getStatusColor(status: string): "default" | "secondary" | "destructive" | "outline" | "success" {
54
+ switch (status) {
55
+ case "completed":
56
+ return "success";
57
+ case "running":
58
+ return "default";
59
+ case "failed":
60
+ return "destructive";
61
+ case "cancelled":
62
+ return "secondary";
63
+ default:
64
+ return "outline";
65
+ }
66
+ }
67
+
68
+ function getStepBgClass(status: string): string {
69
+ switch (status) {
70
+ case "completed":
71
+ return "bg-green-500 text-white";
72
+ case "running":
73
+ return "bg-blue-500 text-white animate-pulse";
74
+ case "failed":
75
+ return "bg-red-500 text-white";
76
+ default:
77
+ return "bg-muted text-muted-foreground";
78
+ }
79
+ }
80
+
81
+ function getStepBorderClass(status: string): string {
82
+ switch (status) {
83
+ case "completed":
84
+ return "border-green-500";
85
+ case "running":
86
+ return "border-blue-500";
87
+ case "failed":
88
+ return "border-red-500";
89
+ default:
90
+ return "border-muted";
91
+ }
92
+ }
93
+
94
+ async function startWorkflow() {
95
+ isStarting = true;
96
+ stepStatuses = {};
97
+ stepResults = {};
98
+ workflowProgress = 0;
99
+ currentStep = null;
100
+ eventLog = [];
101
+
102
+ try {
103
+ const result = await client.api.workflow.start({});
104
+ activeWorkflow = { id: result.instanceId, status: "pending" };
105
+
106
+ // Subscribe to this specific workflow's SSE channel
107
+ const unsubscribe = client.sse.subscribe(
108
+ [`workflow:${result.instanceId}`],
109
+ handleWorkflowEvent
110
+ );
111
+
112
+ // Store unsubscribe for cleanup
113
+ (activeWorkflow as any)._unsubscribe = unsubscribe;
114
+ } catch (e) {
115
+ console.error("Failed to start workflow:", e);
116
+ } finally {
117
+ isStarting = false;
118
+ }
119
+ }
120
+
121
+ function handleWorkflowEvent(eventType: string, eventData: any) {
122
+ // Log the event
123
+ eventLog = [
124
+ { time: new Date().toLocaleTimeString(), event: eventType, data: eventData },
125
+ ...eventLog,
126
+ ].slice(0, 20);
127
+
128
+ switch (eventType) {
129
+ case "workflow.started":
130
+ activeWorkflow = { ...activeWorkflow, status: "running" };
131
+ break;
132
+
133
+ case "workflow.progress":
134
+ workflowProgress = eventData.progress || 0;
135
+ currentStep = eventData.currentStep;
136
+ break;
137
+
138
+ case "workflow.step.started":
139
+ stepStatuses = { ...stepStatuses, [eventData.stepName]: "running" };
140
+ currentStep = eventData.stepName;
141
+ break;
142
+
143
+ case "workflow.step.completed":
144
+ stepStatuses = { ...stepStatuses, [eventData.stepName]: "completed" };
145
+ if (eventData.output) {
146
+ stepResults = { ...stepResults, [eventData.stepName]: eventData.output };
147
+ }
148
+ break;
149
+
150
+ case "workflow.step.failed":
151
+ stepStatuses = { ...stepStatuses, [eventData.stepName]: "failed" };
152
+ break;
153
+
154
+ case "workflow.completed":
155
+ activeWorkflow = { ...activeWorkflow, status: "completed", output: eventData.output };
156
+ workflowProgress = 100;
157
+ refreshInstances();
158
+ break;
159
+
160
+ case "workflow.failed":
161
+ activeWorkflow = { ...activeWorkflow, status: "failed", error: eventData.error };
162
+ refreshInstances();
163
+ break;
164
+
165
+ case "workflow.cancelled":
166
+ activeWorkflow = { ...activeWorkflow, status: "cancelled" };
167
+ refreshInstances();
168
+ break;
169
+ }
170
+ }
171
+
172
+ async function cancelWorkflow() {
173
+ if (!activeWorkflow) return;
174
+ try {
175
+ await client.api.workflow.cancel({ instanceId: activeWorkflow.id });
176
+ } catch (e) {
177
+ console.error("Failed to cancel workflow:", e);
178
+ }
179
+ }
180
+
181
+ async function refreshInstances() {
182
+ try {
183
+ const result = await client.api.workflow.list({});
184
+ instances = result.instances || [];
185
+ } catch (e) {
186
+ console.error("Failed to refresh instances:", e);
187
+ }
188
+ }
189
+
190
+ async function viewWorkflowDetails(instanceId: string) {
191
+ try {
192
+ const status = await client.api.workflow.status({ instanceId });
193
+ if (status) {
194
+ activeWorkflow = status;
195
+ stepStatuses = {};
196
+ stepResults = {};
197
+ workflowProgress = status.status === "completed" ? 100 : 0;
198
+
199
+ // Rebuild step statuses from stepResults
200
+ if (status.stepResults) {
201
+ for (const [stepName, result] of Object.entries(status.stepResults as Record<string, any>)) {
202
+ stepStatuses[stepName] = result.status || "completed";
203
+ if (result.output) {
204
+ stepResults[stepName] = result.output;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ } catch (e) {
210
+ console.error("Failed to get workflow status:", e);
211
+ }
212
+ }
213
+
214
+ function clearWorkflow() {
215
+ if (activeWorkflow?._unsubscribe) {
216
+ activeWorkflow._unsubscribe();
217
+ }
218
+ activeWorkflow = null;
219
+ stepStatuses = {};
220
+ stepResults = {};
221
+ workflowProgress = 0;
222
+ currentStep = null;
223
+ eventLog = [];
224
+ }
225
+
226
+ onMount(() => {
227
+ if (!browser) return;
228
+
229
+ // Subscribe to workflow-updates channel for list updates
230
+ const unsubscribe = client.sse.subscribe(
231
+ ["workflow-updates"],
232
+ (eventType, eventData) => {
233
+ if (["workflow.completed", "workflow.failed", "workflow.cancelled"].includes(eventType)) {
234
+ refreshInstances();
235
+ }
236
+ }
237
+ );
238
+
239
+ return () => {
240
+ unsubscribe();
241
+ if (activeWorkflow?._unsubscribe) {
242
+ activeWorkflow._unsubscribe();
243
+ }
244
+ };
245
+ });
246
+ </script>
247
+
248
+ <div class="min-h-screen bg-background">
249
+ <div class="container mx-auto max-w-6xl py-8 px-4">
250
+ <!-- Header -->
251
+ <div class="text-center mb-8">
252
+ <h1 class="text-3xl font-bold tracking-tight">Workflow Demo</h1>
253
+ <p class="text-muted-foreground mt-2">
254
+ Step Function Orchestration with Real-time Progress
255
+ </p>
256
+ <div class="flex gap-2 justify-center mt-4">
257
+ <a href="/">
258
+ <Button variant="outline" size="sm">Back to Main Demo</Button>
259
+ </a>
260
+ </div>
261
+ </div>
262
+
263
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
264
+ <!-- Workflow Runner -->
265
+ <div class="lg:col-span-2 space-y-6">
266
+ <!-- Control Panel -->
267
+ <Card>
268
+ <CardHeader>
269
+ <CardTitle>Order Processing Workflow</CardTitle>
270
+ <CardDescription>
271
+ Multi-step workflow with validation, payment, and parallel fulfillment
272
+ </CardDescription>
273
+ </CardHeader>
274
+ <CardContent>
275
+ <div class="flex gap-3 items-center">
276
+ <Button
277
+ onclick={startWorkflow}
278
+ disabled={isStarting || (activeWorkflow && activeWorkflow.status === "running")}
279
+ >
280
+ {isStarting ? "Starting..." : "Start New Workflow"}
281
+ </Button>
282
+ {#if activeWorkflow && activeWorkflow.status === "running"}
283
+ <Button variant="destructive" onclick={cancelWorkflow}>
284
+ Cancel
285
+ </Button>
286
+ {/if}
287
+ {#if activeWorkflow && activeWorkflow.status !== "running"}
288
+ <Button variant="outline" onclick={clearWorkflow}>
289
+ Clear
290
+ </Button>
291
+ {/if}
292
+ {#if activeWorkflow}
293
+ <Badge variant={getStatusColor(activeWorkflow.status)}>
294
+ {activeWorkflow.status.toUpperCase()}
295
+ </Badge>
296
+ {/if}
297
+ </div>
298
+ </CardContent>
299
+ </Card>
300
+
301
+ <!-- Progress Visualization -->
302
+ {#if activeWorkflow}
303
+ <Card>
304
+ <CardHeader>
305
+ <CardTitle class="text-lg">Progress</CardTitle>
306
+ <CardDescription>
307
+ Instance: <code class="bg-muted px-1 rounded text-xs">{activeWorkflow.id}</code>
308
+ </CardDescription>
309
+ </CardHeader>
310
+ <CardContent>
311
+ <!-- Progress Bar -->
312
+ <div class="mb-6">
313
+ <div class="flex justify-between text-sm mb-1">
314
+ <span>Progress</span>
315
+ <span>{Math.round(workflowProgress)}%</span>
316
+ </div>
317
+ <div class="h-2 bg-muted rounded-full overflow-hidden">
318
+ <div
319
+ class="h-full bg-primary transition-all duration-500 ease-out"
320
+ style="width: {workflowProgress}%"
321
+ ></div>
322
+ </div>
323
+ </div>
324
+
325
+ <!-- Step Visualization -->
326
+ <div class="space-y-4">
327
+ {#each WORKFLOW_STEPS as step, i}
328
+ {@const status = getStepStatus(step.id)}
329
+ <div class="flex items-start gap-4">
330
+ <!-- Step Number/Icon -->
331
+ <div
332
+ class="w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-300 {getStepBgClass(status)}"
333
+ >
334
+ {#if status === "completed"}
335
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
336
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
337
+ </svg>
338
+ {:else if status === "running"}
339
+ <svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
340
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
341
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
342
+ </svg>
343
+ {:else if status === "failed"}
344
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
345
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
346
+ </svg>
347
+ {:else}
348
+ {step.icon}
349
+ {/if}
350
+ </div>
351
+
352
+ <!-- Step Content -->
353
+ <div class="flex-1">
354
+ <div class="flex items-center gap-2">
355
+ <span class="font-medium">{step.label}</span>
356
+ {#if step.parallel}
357
+ <Badge variant="outline" class="text-xs">Parallel</Badge>
358
+ {/if}
359
+ </div>
360
+
361
+ <!-- Parallel Sub-steps -->
362
+ {#if step.parallel && PARALLEL_STEPS[step.id as keyof typeof PARALLEL_STEPS]}
363
+ <div class="mt-2 ml-4 space-y-2">
364
+ {#each PARALLEL_STEPS[step.id as keyof typeof PARALLEL_STEPS] as subStep}
365
+ {@const subStatus = getStepStatus(subStep.id)}
366
+ <div class="flex items-center gap-2 text-sm">
367
+ <div
368
+ class="w-6 h-6 rounded-full flex items-center justify-center text-xs transition-all duration-300 {getStepBgClass(subStatus)}"
369
+ >
370
+ {#if subStatus === "completed"}
371
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
372
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
373
+ </svg>
374
+ {:else if subStatus === "running"}
375
+ <svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
376
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
377
+ </svg>
378
+ {:else}
379
+ -
380
+ {/if}
381
+ </div>
382
+ <span class={subStatus === "completed" ? "text-green-600" : subStatus === "running" ? "text-blue-600" : "text-muted-foreground"}>
383
+ {subStep.label}
384
+ </span>
385
+ </div>
386
+ {/each}
387
+ </div>
388
+ {/if}
389
+
390
+ <!-- Step Result -->
391
+ {#if stepResults[step.id]}
392
+ <pre class="mt-2 text-xs bg-muted p-2 rounded overflow-auto max-h-20">{JSON.stringify(stepResults[step.id], null, 2)}</pre>
393
+ {/if}
394
+ </div>
395
+ </div>
396
+
397
+ <!-- Connector Line -->
398
+ {#if i < WORKFLOW_STEPS.length - 1}
399
+ <div class="ml-5 w-0.5 h-4 bg-muted"></div>
400
+ {/if}
401
+ {/each}
402
+ </div>
403
+
404
+ <!-- Final Output -->
405
+ {#if activeWorkflow.status === "completed" && activeWorkflow.output}
406
+ <div class="mt-6 p-4 bg-green-50 dark:bg-green-950 rounded-lg border border-green-200 dark:border-green-800">
407
+ <h4 class="font-medium text-green-800 dark:text-green-200 mb-2">Workflow Complete</h4>
408
+ <pre class="text-xs overflow-auto">{JSON.stringify(activeWorkflow.output, null, 2)}</pre>
409
+ </div>
410
+ {/if}
411
+
412
+ {#if activeWorkflow.status === "failed" && activeWorkflow.error}
413
+ <div class="mt-6 p-4 bg-red-50 dark:bg-red-950 rounded-lg border border-red-200 dark:border-red-800">
414
+ <h4 class="font-medium text-red-800 dark:text-red-200 mb-2">Workflow Failed</h4>
415
+ <p class="text-sm text-red-600 dark:text-red-400">{activeWorkflow.error}</p>
416
+ </div>
417
+ {/if}
418
+ </CardContent>
419
+ </Card>
420
+ {/if}
421
+
422
+ <!-- Event Log -->
423
+ {#if eventLog.length > 0}
424
+ <Card>
425
+ <CardHeader>
426
+ <CardTitle class="text-lg">SSE Event Log</CardTitle>
427
+ <CardDescription>Real-time events from the server</CardDescription>
428
+ </CardHeader>
429
+ <CardContent>
430
+ <ul class="space-y-1 max-h-48 overflow-y-auto font-mono text-xs">
431
+ {#each eventLog as log}
432
+ <li class="flex gap-2 p-1 hover:bg-muted rounded">
433
+ <span class="text-muted-foreground">{log.time}</span>
434
+ <Badge variant="outline" class="text-xs">{log.event.replace("workflow.", "")}</Badge>
435
+ {#if log.data.stepName}
436
+ <span class="text-primary">{log.data.stepName}</span>
437
+ {/if}
438
+ {#if log.data.progress !== undefined}
439
+ <span class="text-green-600">{log.data.progress}%</span>
440
+ {/if}
441
+ </li>
442
+ {/each}
443
+ </ul>
444
+ </CardContent>
445
+ </Card>
446
+ {/if}
447
+ </div>
448
+
449
+ <!-- Sidebar - History -->
450
+ <div class="space-y-6">
451
+ <Card>
452
+ <CardHeader>
453
+ <CardTitle class="text-lg">Workflow History</CardTitle>
454
+ <CardDescription>Recent workflow instances</CardDescription>
455
+ </CardHeader>
456
+ <CardContent>
457
+ <div class="flex gap-2 mb-4">
458
+ <Button size="sm" variant="outline" onclick={refreshInstances}>
459
+ Refresh
460
+ </Button>
461
+ </div>
462
+ {#if instances.length === 0}
463
+ <p class="text-sm text-muted-foreground italic">No workflows yet</p>
464
+ {:else}
465
+ <ul class="space-y-2 max-h-96 overflow-y-auto">
466
+ {#each instances as instance}
467
+ <li>
468
+ <button
469
+ class="w-full text-left p-3 rounded-lg border bg-card hover:bg-muted/50 transition-colors"
470
+ onclick={() => viewWorkflowDetails(instance.id)}
471
+ >
472
+ <div class="flex items-center justify-between">
473
+ <code class="text-xs truncate max-w-[120px]">{instance.id}</code>
474
+ <Badge variant={getStatusColor(instance.status)} class="text-xs">
475
+ {instance.status}
476
+ </Badge>
477
+ </div>
478
+ {#if instance.currentStep}
479
+ <p class="text-xs text-muted-foreground mt-1">
480
+ Step: {instance.currentStep}
481
+ </p>
482
+ {/if}
483
+ <p class="text-xs text-muted-foreground mt-1">
484
+ {new Date(instance.createdAt).toLocaleString()}
485
+ </p>
486
+ </button>
487
+ </li>
488
+ {/each}
489
+ </ul>
490
+ {/if}
491
+ </CardContent>
492
+ </Card>
493
+
494
+ <!-- Workflow Info -->
495
+ <Card>
496
+ <CardHeader>
497
+ <CardTitle class="text-lg">About This Demo</CardTitle>
498
+ </CardHeader>
499
+ <CardContent class="text-sm space-y-2">
500
+ <p>
501
+ This demo shows a <strong>step function workflow</strong> for order processing:
502
+ </p>
503
+ <ol class="list-decimal list-inside space-y-1 text-muted-foreground">
504
+ <li>Validate order (1s)</li>
505
+ <li>Process payment (2s)</li>
506
+ <li>Parallel fulfillment:
507
+ <ul class="list-disc list-inside ml-4">
508
+ <li>Send confirmation email</li>
509
+ <li>Prepare shipment</li>
510
+ </ul>
511
+ </li>
512
+ <li>Complete with summary</li>
513
+ </ol>
514
+ <p class="text-muted-foreground">
515
+ Progress is streamed via <strong>Server-Sent Events (SSE)</strong> for real-time updates.
516
+ </p>
517
+ </CardContent>
518
+ </Card>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ </div>
@@ -4,6 +4,7 @@ import { Kysely } from "kysely";
4
4
  import { BunSqliteDialect } from "kysely-bun-sqlite";
5
5
  import { Database } from "bun:sqlite";
6
6
  import { demoPlugin } from "./plugins/demo";
7
+ import { workflowDemoPlugin } from "./plugins/workflow-demo";
7
8
  import demoRoutes from "./routes/demo";
8
9
 
9
10
  // Simple in-memory database
@@ -20,8 +21,9 @@ export const server = new AppServer({
20
21
  },
21
22
  });
22
23
 
23
- // Register plugin
24
+ // Register plugins
24
25
  server.registerPlugin(demoPlugin);
26
+ server.registerPlugin(workflowDemoPlugin);
25
27
 
26
28
  // Register routes
27
29
  server.use(demoRoutes);