@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.
- package/package.json +2 -2
- package/src/client/base.ts +6 -4
- package/templates/sveltekit-app/bun.lock +170 -34
- package/templates/sveltekit-app/package.json +3 -3
- package/templates/sveltekit-app/src/lib/api.ts +109 -40
- package/templates/sveltekit-app/src/routes/+page.svelte +10 -3
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +23 -0
- package/templates/sveltekit-app/src/routes/workflows/+page.svelte +522 -0
- package/templates/sveltekit-app/src/server/index.ts +3 -1
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +197 -0
- package/templates/sveltekit-app/src/server/routes/demo.ts +82 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Workflow Demo Plugin - Demonstrates step function orchestration
|
|
2
|
+
import { createPlugin, workflow, type WorkflowContext, type CoreServices } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// Helper to simulate async work with delay
|
|
6
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
|
|
8
|
+
// Input/output types for better type safety
|
|
9
|
+
type OrderInput = {
|
|
10
|
+
orderId: string;
|
|
11
|
+
items: { name: string; qty: number }[];
|
|
12
|
+
customerEmail: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ValidateOutput = { valid: boolean; total: number; itemCount: number };
|
|
16
|
+
type PaymentOutput = { paymentId: string; status: string };
|
|
17
|
+
|
|
18
|
+
// Define an example order processing workflow with placeholder tasks
|
|
19
|
+
export const orderWorkflow = workflow("process-order")
|
|
20
|
+
.timeout(60000) // 1 minute max
|
|
21
|
+
.defaultRetry({ maxAttempts: 2 })
|
|
22
|
+
|
|
23
|
+
// Step 1: Validate Order
|
|
24
|
+
.task("validate", {
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
orderId: z.string(),
|
|
27
|
+
items: z.array(z.object({ name: z.string(), qty: z.number() })),
|
|
28
|
+
customerEmail: z.string().email(),
|
|
29
|
+
}),
|
|
30
|
+
outputSchema: z.object({
|
|
31
|
+
valid: z.boolean(),
|
|
32
|
+
total: z.number(),
|
|
33
|
+
itemCount: z.number(),
|
|
34
|
+
}),
|
|
35
|
+
handler: async (input: OrderInput, ctx: { core: CoreServices }): Promise<ValidateOutput> => {
|
|
36
|
+
ctx.core.logger.info("Validating order", { orderId: input.orderId });
|
|
37
|
+
await sleep(1000); // Simulate validation work
|
|
38
|
+
const total = input.items.reduce((sum: number, item: { qty: number }) => sum + item.qty * 10, 0);
|
|
39
|
+
return { valid: true, total, itemCount: input.items.length };
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Step 2: Process Payment
|
|
44
|
+
.task("payment", {
|
|
45
|
+
inputSchema: (prev: ValidateOutput, workflowInput: OrderInput) => ({
|
|
46
|
+
orderId: workflowInput.orderId,
|
|
47
|
+
amount: prev.total,
|
|
48
|
+
email: workflowInput.customerEmail,
|
|
49
|
+
}),
|
|
50
|
+
outputSchema: z.object({
|
|
51
|
+
paymentId: z.string(),
|
|
52
|
+
status: z.string(),
|
|
53
|
+
}),
|
|
54
|
+
handler: async (input: { orderId: string; amount: number; email: string }, ctx: { core: CoreServices }): Promise<PaymentOutput> => {
|
|
55
|
+
ctx.core.logger.info("Processing payment", {
|
|
56
|
+
orderId: input.orderId,
|
|
57
|
+
amount: input.amount,
|
|
58
|
+
});
|
|
59
|
+
await sleep(2000); // Simulate payment processing
|
|
60
|
+
return {
|
|
61
|
+
paymentId: `PAY-${Date.now().toString(36).toUpperCase()}`,
|
|
62
|
+
status: "completed",
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Step 3: Parallel - Send Notification + Prepare Shipment
|
|
68
|
+
.parallel("fulfill", {
|
|
69
|
+
branches: [
|
|
70
|
+
workflow
|
|
71
|
+
.branch("notification")
|
|
72
|
+
.task("send-email", {
|
|
73
|
+
handler: async (_input: unknown, ctx: { core: CoreServices }) => {
|
|
74
|
+
ctx.core.logger.info("Sending confirmation email");
|
|
75
|
+
await sleep(800); // Simulate email send
|
|
76
|
+
return { emailSent: true, sentAt: new Date().toISOString() };
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
.build(),
|
|
80
|
+
|
|
81
|
+
workflow
|
|
82
|
+
.branch("shipping")
|
|
83
|
+
.task("prepare-shipment", {
|
|
84
|
+
handler: async (_input: unknown, ctx: { core: CoreServices }) => {
|
|
85
|
+
ctx.core.logger.info("Preparing shipment");
|
|
86
|
+
await sleep(1500); // Simulate shipment prep
|
|
87
|
+
return {
|
|
88
|
+
trackingId: `SHIP-${Date.now().toString(36).toUpperCase()}`,
|
|
89
|
+
carrier: "FastShip",
|
|
90
|
+
estimatedDelivery: new Date(
|
|
91
|
+
Date.now() + 3 * 24 * 60 * 60 * 1000
|
|
92
|
+
).toISOString(),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
.build(),
|
|
97
|
+
],
|
|
98
|
+
next: "complete",
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Step 4: Complete
|
|
102
|
+
.pass("complete", {
|
|
103
|
+
transform: (ctx: WorkflowContext) => ({
|
|
104
|
+
orderId: ctx.input.orderId,
|
|
105
|
+
paymentId: ctx.steps["payment"].paymentId,
|
|
106
|
+
tracking: ctx.steps["fulfill"].shipping["prepare-shipment"].trackingId,
|
|
107
|
+
emailSent: ctx.steps["fulfill"].notification["send-email"].emailSent,
|
|
108
|
+
completedAt: new Date().toISOString(),
|
|
109
|
+
}),
|
|
110
|
+
end: true,
|
|
111
|
+
})
|
|
112
|
+
.build();
|
|
113
|
+
|
|
114
|
+
// Plugin that registers the workflow and provides service methods
|
|
115
|
+
export const workflowDemoPlugin = createPlugin.define({
|
|
116
|
+
name: "workflowDemo",
|
|
117
|
+
service: async (ctx) => ({
|
|
118
|
+
// Start a new order processing workflow
|
|
119
|
+
startOrder: async (input: {
|
|
120
|
+
orderId: string;
|
|
121
|
+
items: { name: string; qty: number }[];
|
|
122
|
+
customerEmail: string;
|
|
123
|
+
}) => {
|
|
124
|
+
const instanceId = await ctx.core.workflows.start("process-order", input);
|
|
125
|
+
return { instanceId };
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Get workflow instance status
|
|
129
|
+
getStatus: async (instanceId: string) => {
|
|
130
|
+
const instance = await ctx.core.workflows.getInstance(instanceId);
|
|
131
|
+
if (!instance) return null;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: instance.id,
|
|
135
|
+
status: instance.status,
|
|
136
|
+
currentStep: instance.currentStep,
|
|
137
|
+
input: instance.input,
|
|
138
|
+
output: instance.output,
|
|
139
|
+
error: instance.error,
|
|
140
|
+
stepResults: instance.stepResults,
|
|
141
|
+
createdAt: instance.createdAt.toISOString(),
|
|
142
|
+
startedAt: instance.startedAt?.toISOString(),
|
|
143
|
+
completedAt: instance.completedAt?.toISOString(),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// List all workflow instances
|
|
148
|
+
listInstances: async (status?: string) => {
|
|
149
|
+
const instances = await ctx.core.workflows.getInstances(
|
|
150
|
+
"process-order",
|
|
151
|
+
status as any
|
|
152
|
+
);
|
|
153
|
+
return instances.map((i) => ({
|
|
154
|
+
id: i.id,
|
|
155
|
+
status: i.status,
|
|
156
|
+
currentStep: i.currentStep,
|
|
157
|
+
createdAt: i.createdAt.toISOString(),
|
|
158
|
+
completedAt: i.completedAt?.toISOString(),
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// Cancel a running workflow
|
|
163
|
+
cancel: async (instanceId: string) => {
|
|
164
|
+
const success = await ctx.core.workflows.cancel(instanceId);
|
|
165
|
+
return { success };
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
|
|
169
|
+
init: async (ctx) => {
|
|
170
|
+
// Register the workflow definition
|
|
171
|
+
ctx.core.workflows.register(orderWorkflow);
|
|
172
|
+
|
|
173
|
+
// Broadcast workflow events to SSE for real-time UI updates
|
|
174
|
+
const workflowEvents = [
|
|
175
|
+
"workflow.started",
|
|
176
|
+
"workflow.progress",
|
|
177
|
+
"workflow.completed",
|
|
178
|
+
"workflow.failed",
|
|
179
|
+
"workflow.cancelled",
|
|
180
|
+
"workflow.step.started",
|
|
181
|
+
"workflow.step.completed",
|
|
182
|
+
"workflow.step.failed",
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const event of workflowEvents) {
|
|
186
|
+
ctx.core.events.on(event, (data: any) => {
|
|
187
|
+
// Broadcast to workflow-specific channel
|
|
188
|
+
ctx.core.sse.broadcast(`workflow:${data.instanceId}`, event, data);
|
|
189
|
+
|
|
190
|
+
// Also broadcast to general workflow-updates channel for the demo list
|
|
191
|
+
ctx.core.sse.broadcast("workflow-updates", event, data);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ctx.core.logger.info("Workflow demo plugin initialized with order workflow");
|
|
196
|
+
},
|
|
197
|
+
});
|
|
@@ -234,4 +234,86 @@ demo.route("cron.list").typed(
|
|
|
234
234
|
})
|
|
235
235
|
);
|
|
236
236
|
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// WORKFLOWS - Step function orchestration (via workflowDemo plugin)
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
demo.route("workflow.start").typed(
|
|
242
|
+
defineRoute({
|
|
243
|
+
input: z.object({
|
|
244
|
+
orderId: z.string().default(() => `ORD-${Date.now().toString(36).toUpperCase()}`),
|
|
245
|
+
items: z
|
|
246
|
+
.array(z.object({ name: z.string(), qty: z.number() }))
|
|
247
|
+
.default([
|
|
248
|
+
{ name: "Widget A", qty: 2 },
|
|
249
|
+
{ name: "Gadget B", qty: 1 },
|
|
250
|
+
]),
|
|
251
|
+
customerEmail: z.string().email().default("demo@example.com"),
|
|
252
|
+
}),
|
|
253
|
+
output: z.object({ instanceId: z.string() }),
|
|
254
|
+
handle: async (input, ctx) => {
|
|
255
|
+
return ctx.plugins.workflowDemo.startOrder({
|
|
256
|
+
orderId: input.orderId ?? `ORD-${Date.now().toString(36).toUpperCase()}`,
|
|
257
|
+
items: input.items ?? [
|
|
258
|
+
{ name: "Widget A", qty: 2 },
|
|
259
|
+
{ name: "Gadget B", qty: 1 },
|
|
260
|
+
],
|
|
261
|
+
customerEmail: input.customerEmail ?? "demo@example.com",
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
demo.route("workflow.status").typed(
|
|
268
|
+
defineRoute({
|
|
269
|
+
input: z.object({ instanceId: z.string() }),
|
|
270
|
+
output: z.object({
|
|
271
|
+
id: z.string(),
|
|
272
|
+
status: z.string(),
|
|
273
|
+
currentStep: z.string().optional(),
|
|
274
|
+
input: z.any(),
|
|
275
|
+
output: z.any().optional(),
|
|
276
|
+
error: z.string().optional(),
|
|
277
|
+
stepResults: z.record(z.any()),
|
|
278
|
+
createdAt: z.string(),
|
|
279
|
+
startedAt: z.string().optional(),
|
|
280
|
+
completedAt: z.string().optional(),
|
|
281
|
+
}).nullable(),
|
|
282
|
+
handle: async (input, ctx) => {
|
|
283
|
+
return ctx.plugins.workflowDemo.getStatus(input.instanceId);
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
demo.route("workflow.list").typed(
|
|
289
|
+
defineRoute({
|
|
290
|
+
input: z.object({ status: z.string().optional() }),
|
|
291
|
+
output: z.object({
|
|
292
|
+
instances: z.array(
|
|
293
|
+
z.object({
|
|
294
|
+
id: z.string(),
|
|
295
|
+
status: z.string(),
|
|
296
|
+
currentStep: z.string().optional(),
|
|
297
|
+
createdAt: z.string(),
|
|
298
|
+
completedAt: z.string().optional(),
|
|
299
|
+
})
|
|
300
|
+
),
|
|
301
|
+
}),
|
|
302
|
+
handle: async (input, ctx) => {
|
|
303
|
+
const instances = await ctx.plugins.workflowDemo.listInstances(input.status);
|
|
304
|
+
return { instances };
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
demo.route("workflow.cancel").typed(
|
|
310
|
+
defineRoute({
|
|
311
|
+
input: z.object({ instanceId: z.string() }),
|
|
312
|
+
output: z.object({ success: z.boolean() }),
|
|
313
|
+
handle: async (input, ctx) => {
|
|
314
|
+
return ctx.plugins.workflowDemo.cancel(input.instanceId);
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
|
|
237
319
|
export default demo;
|