@meetploy/cli 1.5.0 → 1.7.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.
package/dist/dev.js CHANGED
@@ -1,2 +1,1065 @@
1
- export { initPloyForDev } from "@meetploy/emulator";
2
- //# sourceMappingURL=dev.js.map
1
+ import { createRequire } from 'module';
2
+ import 'child_process';
3
+ import { readFile, existsSync, readFileSync, mkdirSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import 'esbuild';
7
+ import 'chokidar';
8
+ import { promisify } from 'util';
9
+ import { parse } from 'yaml';
10
+ import { serve } from '@hono/node-server';
11
+ import { Hono } from 'hono';
12
+ import { randomUUID } from 'crypto';
13
+ import 'os';
14
+ import Database from 'better-sqlite3';
15
+
16
+ createRequire(import.meta.url);
17
+ promisify(readFile);
18
+ function readPloyConfigSync(projectDir, configPath) {
19
+ const configFile = configPath;
20
+ const fullPath = join(projectDir, configFile);
21
+ if (!existsSync(fullPath)) {
22
+ throw new Error(`Config file not found: ${fullPath}`);
23
+ }
24
+ const content = readFileSync(fullPath, "utf-8");
25
+ return parse(content);
26
+ }
27
+
28
+ // ../emulator/dist/config/ploy-config.js
29
+ function readPloyConfig(projectDir, configPath) {
30
+ const config = readPloyConfigSync(projectDir, configPath);
31
+ if (!config.kind) {
32
+ throw new Error(`Missing required field 'kind' in ${configPath}`);
33
+ }
34
+ if (config.kind !== "dynamic" && config.kind !== "worker") {
35
+ throw new Error(`Invalid kind '${config.kind}' in ${configPath}. Must be 'dynamic' or 'worker'`);
36
+ }
37
+ return config;
38
+ }
39
+ var __filename = fileURLToPath(import.meta.url);
40
+ var __dirname = dirname(__filename);
41
+ function findDashboardDistPath() {
42
+ const possiblePaths = [
43
+ join(__dirname, "..", "dashboard-dist"),
44
+ join(__dirname, "..", "..", "src", "dashboard-dist")
45
+ ];
46
+ for (const p of possiblePaths) {
47
+ if (existsSync(p)) {
48
+ return p;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ var MIME_TYPES = {
54
+ ".html": "text/html",
55
+ ".js": "application/javascript",
56
+ ".css": "text/css",
57
+ ".json": "application/json",
58
+ ".png": "image/png",
59
+ ".jpg": "image/jpeg",
60
+ ".svg": "image/svg+xml",
61
+ ".ico": "image/x-icon",
62
+ ".woff": "font/woff",
63
+ ".woff2": "font/woff2"
64
+ };
65
+ function getMimeType(filePath) {
66
+ const ext = filePath.substring(filePath.lastIndexOf("."));
67
+ return MIME_TYPES[ext] || "application/octet-stream";
68
+ }
69
+ function createDashboardRoutes(app, dbManager2, config) {
70
+ const dashboardDistPath = findDashboardDistPath();
71
+ const hasDashboard = dashboardDistPath !== null;
72
+ function getDbResourceName(bindingName) {
73
+ return config.db?.[bindingName] ?? null;
74
+ }
75
+ app.get("/api/config", (c) => {
76
+ return c.json({
77
+ db: config.db,
78
+ queue: config.queue,
79
+ workflow: config.workflow
80
+ });
81
+ });
82
+ app.post("/api/db/:binding/query", async (c) => {
83
+ const binding = c.req.param("binding");
84
+ const resourceName = getDbResourceName(binding);
85
+ if (!resourceName) {
86
+ return c.json({ error: `Database binding '${binding}' not found` }, 404);
87
+ }
88
+ const body = await c.req.json();
89
+ const { query } = body;
90
+ if (!query) {
91
+ return c.json({ error: "Query is required" }, 400);
92
+ }
93
+ try {
94
+ const db = dbManager2.getD1Database(resourceName);
95
+ const startTime = Date.now();
96
+ const stmt = db.prepare(query);
97
+ const isSelect = query.trim().toUpperCase().startsWith("SELECT");
98
+ let results = [];
99
+ let changes = 0;
100
+ if (isSelect) {
101
+ results = stmt.all();
102
+ } else {
103
+ const info = stmt.run();
104
+ changes = info.changes;
105
+ }
106
+ const duration = Date.now() - startTime;
107
+ return c.json({
108
+ results,
109
+ success: true,
110
+ meta: {
111
+ duration,
112
+ rows_read: results.length,
113
+ rows_written: changes
114
+ }
115
+ });
116
+ } catch (err) {
117
+ return c.json({
118
+ results: [],
119
+ success: false,
120
+ error: err instanceof Error ? err.message : String(err),
121
+ meta: { duration: 0, rows_read: 0, rows_written: 0 }
122
+ }, 400);
123
+ }
124
+ });
125
+ app.get("/api/db/:binding/tables", (c) => {
126
+ const binding = c.req.param("binding");
127
+ const resourceName = getDbResourceName(binding);
128
+ if (!resourceName) {
129
+ return c.json({ error: `Database binding '${binding}' not found` }, 404);
130
+ }
131
+ try {
132
+ const db = dbManager2.getD1Database(resourceName);
133
+ const tables = db.prepare(`SELECT name FROM sqlite_master
134
+ WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%'
135
+ ORDER BY name`).all();
136
+ return c.json({ tables });
137
+ } catch (err) {
138
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
139
+ }
140
+ });
141
+ app.get("/api/db/:binding/tables/:tableName", (c) => {
142
+ const binding = c.req.param("binding");
143
+ const resourceName = getDbResourceName(binding);
144
+ if (!resourceName) {
145
+ return c.json({ error: `Database binding '${binding}' not found` }, 404);
146
+ }
147
+ const tableName = c.req.param("tableName");
148
+ const limit = parseInt(c.req.query("limit") || "50", 10);
149
+ const offset = parseInt(c.req.query("offset") || "0", 10);
150
+ try {
151
+ const db = dbManager2.getD1Database(resourceName);
152
+ const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
153
+ const columns = columnsResult.map((col) => col.name);
154
+ const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
155
+ const total = countResult.count;
156
+ const data = db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
157
+ return c.json({ data, columns, total });
158
+ } catch (err) {
159
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
160
+ }
161
+ });
162
+ app.get("/api/db/:binding/schema", (c) => {
163
+ const binding = c.req.param("binding");
164
+ const resourceName = getDbResourceName(binding);
165
+ if (!resourceName) {
166
+ return c.json({ error: `Database binding '${binding}' not found` }, 404);
167
+ }
168
+ try {
169
+ const db = dbManager2.getD1Database(resourceName);
170
+ const tablesResult = db.prepare(`SELECT name FROM sqlite_master
171
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
172
+ ORDER BY name`).all();
173
+ const tables = tablesResult.map((table) => {
174
+ const columnsResult = db.prepare(`PRAGMA table_info("${table.name}")`).all();
175
+ return {
176
+ name: table.name,
177
+ columns: columnsResult.map((col) => ({
178
+ name: col.name,
179
+ type: col.type,
180
+ notNull: col.notnull === 1,
181
+ primaryKey: col.pk === 1
182
+ }))
183
+ };
184
+ });
185
+ return c.json({ tables });
186
+ } catch (err) {
187
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
188
+ }
189
+ });
190
+ app.get("/api/queue/:binding/metrics", (c) => {
191
+ const binding = c.req.param("binding");
192
+ const queueName = config.queue?.[binding];
193
+ if (!queueName) {
194
+ return c.json({ error: "Queue not found" }, 404);
195
+ }
196
+ try {
197
+ const db = dbManager2.emulatorDb;
198
+ const metrics = {
199
+ pending: 0,
200
+ processing: 0,
201
+ acknowledged: 0,
202
+ failed: 0,
203
+ deadLetter: 0
204
+ };
205
+ const statusCounts = db.prepare(`SELECT status, COUNT(*) as count
206
+ FROM queue_messages
207
+ WHERE queue_name = ?
208
+ GROUP BY status`).all(queueName);
209
+ for (const row of statusCounts) {
210
+ if (row.status === "pending") {
211
+ metrics.pending = row.count;
212
+ } else if (row.status === "processing") {
213
+ metrics.processing = row.count;
214
+ } else if (row.status === "acknowledged") {
215
+ metrics.acknowledged = row.count;
216
+ } else if (row.status === "failed") {
217
+ metrics.failed = row.count;
218
+ } else if (row.status === "dead_letter") {
219
+ metrics.deadLetter = row.count;
220
+ }
221
+ }
222
+ return c.json({ metrics });
223
+ } catch (err) {
224
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
225
+ }
226
+ });
227
+ app.get("/api/queue/:binding/messages", (c) => {
228
+ const binding = c.req.param("binding");
229
+ const queueName = config.queue?.[binding];
230
+ const limit = parseInt(c.req.query("limit") || "10", 10);
231
+ if (!queueName) {
232
+ return c.json({ error: "Queue not found" }, 404);
233
+ }
234
+ try {
235
+ const db = dbManager2.emulatorDb;
236
+ const messages = db.prepare(`SELECT id, status, payload, attempt, created_at
237
+ FROM queue_messages
238
+ WHERE queue_name = ?
239
+ ORDER BY created_at DESC
240
+ LIMIT ?`).all(queueName, limit);
241
+ return c.json({
242
+ messages: messages.map((m) => ({
243
+ id: m.id,
244
+ status: m.status.toUpperCase(),
245
+ payload: JSON.parse(m.payload),
246
+ attempt: m.attempt,
247
+ createdAt: new Date(m.created_at * 1e3).toISOString()
248
+ }))
249
+ });
250
+ } catch (err) {
251
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
252
+ }
253
+ });
254
+ app.get("/api/workflow/:binding/executions", (c) => {
255
+ const binding = c.req.param("binding");
256
+ const workflowConfig = config.workflow?.[binding];
257
+ const limit = parseInt(c.req.query("limit") || "20", 10);
258
+ if (!workflowConfig) {
259
+ return c.json({ error: "Workflow not found" }, 404);
260
+ }
261
+ try {
262
+ const db = dbManager2.emulatorDb;
263
+ const workflowName = workflowConfig;
264
+ const executions = db.prepare(`SELECT
265
+ e.id,
266
+ e.workflow_name,
267
+ e.status,
268
+ e.error,
269
+ e.started_at,
270
+ e.completed_at,
271
+ e.created_at,
272
+ (SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id) as steps_count,
273
+ (SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id AND status = 'completed') as steps_completed
274
+ FROM workflow_executions e
275
+ WHERE e.workflow_name = ?
276
+ ORDER BY e.created_at DESC
277
+ LIMIT ?`).all(workflowName, limit);
278
+ return c.json({
279
+ executions: executions.map((e) => ({
280
+ id: e.id,
281
+ status: e.status.toUpperCase(),
282
+ startedAt: e.started_at ? new Date(e.started_at * 1e3).toISOString() : null,
283
+ completedAt: e.completed_at ? new Date(e.completed_at * 1e3).toISOString() : null,
284
+ durationMs: e.started_at && e.completed_at ? (e.completed_at - e.started_at) * 1e3 : null,
285
+ stepsCount: e.steps_count,
286
+ stepsCompleted: e.steps_completed,
287
+ errorMessage: e.error,
288
+ createdAt: new Date(e.created_at * 1e3).toISOString()
289
+ }))
290
+ });
291
+ } catch (err) {
292
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
293
+ }
294
+ });
295
+ app.get("/api/workflow/:binding/executions/:executionId", (c) => {
296
+ const binding = c.req.param("binding");
297
+ const executionId = c.req.param("executionId");
298
+ const workflowConfig = config.workflow?.[binding];
299
+ if (!workflowConfig) {
300
+ return c.json({ error: "Workflow not found" }, 404);
301
+ }
302
+ try {
303
+ const db = dbManager2.emulatorDb;
304
+ const execution = db.prepare(`SELECT id, workflow_name, status, error, started_at, completed_at, created_at
305
+ FROM workflow_executions
306
+ WHERE id = ?`).get(executionId);
307
+ if (!execution) {
308
+ return c.json({ error: "Execution not found" }, 404);
309
+ }
310
+ const steps = db.prepare(`SELECT id, step_name, step_index, status, output, error, duration_ms, created_at
311
+ FROM workflow_steps
312
+ WHERE execution_id = ?
313
+ ORDER BY step_index`).all(executionId);
314
+ return c.json({
315
+ execution: {
316
+ id: execution.id,
317
+ status: execution.status.toUpperCase(),
318
+ startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
319
+ completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
320
+ durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
321
+ stepsCount: steps.length,
322
+ stepsCompleted: steps.filter((s) => s.status === "completed").length,
323
+ errorMessage: execution.error,
324
+ createdAt: new Date(execution.created_at * 1e3).toISOString()
325
+ },
326
+ steps: steps.map((s) => ({
327
+ id: String(s.id),
328
+ stepName: s.step_name,
329
+ stepIndex: s.step_index,
330
+ status: s.status.toUpperCase(),
331
+ output: s.output ? JSON.parse(s.output) : null,
332
+ errorMessage: s.error,
333
+ durationMs: s.duration_ms,
334
+ createdAt: new Date(s.created_at * 1e3).toISOString()
335
+ }))
336
+ });
337
+ } catch (err) {
338
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
339
+ }
340
+ });
341
+ if (hasDashboard) {
342
+ app.get("/assets/*", (c) => {
343
+ const path = c.req.path;
344
+ const filePath = join(dashboardDistPath, path);
345
+ if (!existsSync(filePath)) {
346
+ return c.notFound();
347
+ }
348
+ const content = readFileSync(filePath);
349
+ const mimeType = getMimeType(filePath);
350
+ return new Response(content, {
351
+ headers: {
352
+ "Content-Type": mimeType,
353
+ "Cache-Control": "public, max-age=31536000"
354
+ }
355
+ });
356
+ });
357
+ app.get("*", (c) => {
358
+ if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/db") || c.req.path.startsWith("/queue") || c.req.path.startsWith("/workflow")) {
359
+ return c.notFound();
360
+ }
361
+ const indexPath = join(dashboardDistPath, "index.html");
362
+ if (!existsSync(indexPath)) {
363
+ return c.text("Dashboard not found. Run 'pnpm build' in packages/dev-dashboard.", 404);
364
+ }
365
+ const content = readFileSync(indexPath, "utf-8");
366
+ return c.html(content);
367
+ });
368
+ }
369
+ }
370
+
371
+ // ../emulator/dist/services/db-service.js
372
+ function createDbHandler(getDatabase) {
373
+ return async (c) => {
374
+ const startTime = Date.now();
375
+ try {
376
+ const body = await c.req.json();
377
+ const { bindingName, method, query, params, statements } = body;
378
+ const db = getDatabase(bindingName);
379
+ if (method === "prepare" && query) {
380
+ const stmt = db.prepare(query);
381
+ const isSelect = query.trim().toUpperCase().startsWith("SELECT");
382
+ let results;
383
+ let changes = 0;
384
+ if (isSelect) {
385
+ results = params?.length ? stmt.all(...params) : stmt.all();
386
+ } else {
387
+ const info = params?.length ? stmt.run(...params) : stmt.run();
388
+ changes = info.changes;
389
+ results = [];
390
+ }
391
+ const duration = Date.now() - startTime;
392
+ return c.json({
393
+ results,
394
+ success: true,
395
+ meta: {
396
+ duration,
397
+ rows_read: results.length,
398
+ rows_written: changes
399
+ }
400
+ });
401
+ }
402
+ if (method === "exec" && query) {
403
+ db.exec(query);
404
+ const duration = Date.now() - startTime;
405
+ return c.json({
406
+ results: [],
407
+ success: true,
408
+ meta: {
409
+ duration,
410
+ rows_read: 0,
411
+ rows_written: 0
412
+ }
413
+ });
414
+ }
415
+ if (method === "batch" && statements) {
416
+ const results = [];
417
+ const transaction = db.transaction(() => {
418
+ for (const stmt of statements) {
419
+ const prepared = db.prepare(stmt.query);
420
+ const isSelect = stmt.query.trim().toUpperCase().startsWith("SELECT");
421
+ if (isSelect) {
422
+ const rows = stmt.params?.length ? prepared.all(...stmt.params) : prepared.all();
423
+ results.push({
424
+ results: rows,
425
+ success: true,
426
+ meta: {
427
+ duration: 0,
428
+ rows_read: rows.length,
429
+ rows_written: 0
430
+ }
431
+ });
432
+ } else {
433
+ const info = stmt.params?.length ? prepared.run(...stmt.params) : prepared.run();
434
+ results.push({
435
+ results: [],
436
+ success: true,
437
+ meta: {
438
+ duration: 0,
439
+ rows_read: 0,
440
+ rows_written: info.changes
441
+ }
442
+ });
443
+ }
444
+ }
445
+ });
446
+ transaction();
447
+ return c.json(results);
448
+ }
449
+ if (method === "dump") {
450
+ const buffer = db.serialize();
451
+ return new Response(new Uint8Array(buffer), {
452
+ headers: {
453
+ "Content-Type": "application/octet-stream"
454
+ }
455
+ });
456
+ }
457
+ return c.json({ error: "Invalid method" }, 400);
458
+ } catch (err) {
459
+ const message = err instanceof Error ? err.message : String(err);
460
+ return c.json({ error: message }, 500);
461
+ }
462
+ };
463
+ }
464
+ function createQueueHandlers(db) {
465
+ const sendHandler = async (c) => {
466
+ try {
467
+ const body = await c.req.json();
468
+ const { queueName, payload, delaySeconds = 0 } = body;
469
+ const id = randomUUID();
470
+ const now = Math.floor(Date.now() / 1e3);
471
+ const visibleAt = now + delaySeconds;
472
+ db.prepare(`INSERT INTO queue_messages (id, queue_name, payload, visible_at)
473
+ VALUES (?, ?, ?, ?)`).run(id, queueName, JSON.stringify(payload), visibleAt);
474
+ return c.json({ success: true, messageId: id });
475
+ } catch (err) {
476
+ const message = err instanceof Error ? err.message : String(err);
477
+ return c.json({ success: false, error: message }, 500);
478
+ }
479
+ };
480
+ const batchSendHandler = async (c) => {
481
+ try {
482
+ const body = await c.req.json();
483
+ const { queueName, messages } = body;
484
+ const now = Math.floor(Date.now() / 1e3);
485
+ const messageIds = [];
486
+ const insert = db.prepare(`INSERT INTO queue_messages (id, queue_name, payload, visible_at)
487
+ VALUES (?, ?, ?, ?)`);
488
+ const transaction = db.transaction(() => {
489
+ for (const msg of messages) {
490
+ const id = randomUUID();
491
+ const visibleAt = now + (msg.delaySeconds || 0);
492
+ insert.run(id, queueName, JSON.stringify(msg.payload), visibleAt);
493
+ messageIds.push(id);
494
+ }
495
+ });
496
+ transaction();
497
+ return c.json({ success: true, messageIds });
498
+ } catch (err) {
499
+ const message = err instanceof Error ? err.message : String(err);
500
+ return c.json({ success: false, error: message }, 500);
501
+ }
502
+ };
503
+ const receiveHandler = async (c) => {
504
+ try {
505
+ const body = await c.req.json();
506
+ const { queueName, maxMessages = 10, visibilityTimeoutSeconds = 30 } = body;
507
+ const now = Math.floor(Date.now() / 1e3);
508
+ const deliveryId = randomUUID();
509
+ const newVisibleAt = now + visibilityTimeoutSeconds;
510
+ const rows = db.prepare(`SELECT id, queue_name, payload, attempt
511
+ FROM queue_messages
512
+ WHERE queue_name = ? AND status = 'pending' AND visible_at <= ?
513
+ ORDER BY visible_at ASC
514
+ LIMIT ?`).all(queueName, now, maxMessages);
515
+ if (rows.length === 0) {
516
+ return c.json({ success: true, messages: [] });
517
+ }
518
+ const ids = rows.map((r) => r.id);
519
+ const placeholders = ids.map(() => "?").join(",");
520
+ db.prepare(`UPDATE queue_messages
521
+ SET status = 'processing', delivery_id = ?, visible_at = ?, attempt = attempt + 1, updated_at = ?
522
+ WHERE id IN (${placeholders})`).run(deliveryId, newVisibleAt, now, ...ids);
523
+ const messages = rows.map((row) => ({
524
+ id: row.id,
525
+ payload: JSON.parse(row.payload),
526
+ attempt: row.attempt + 1,
527
+ deliveryId
528
+ }));
529
+ return c.json({ success: true, messages });
530
+ } catch (err) {
531
+ const message = err instanceof Error ? err.message : String(err);
532
+ return c.json({ success: false, messages: [], error: message }, 500);
533
+ }
534
+ };
535
+ const ackHandler = async (c) => {
536
+ try {
537
+ const body = await c.req.json();
538
+ const { messageId, deliveryId } = body;
539
+ const result = db.prepare(`DELETE FROM queue_messages
540
+ WHERE id = ? AND delivery_id = ?`).run(messageId, deliveryId);
541
+ if (result.changes === 0) {
542
+ return c.json({ success: false, error: "Message not found or already processed" }, 404);
543
+ }
544
+ return c.json({ success: true });
545
+ } catch (err) {
546
+ const message = err instanceof Error ? err.message : String(err);
547
+ return c.json({ success: false, error: message }, 500);
548
+ }
549
+ };
550
+ const retryHandler = async (c) => {
551
+ try {
552
+ const body = await c.req.json();
553
+ const { messageId, deliveryId } = body;
554
+ const now = Math.floor(Date.now() / 1e3);
555
+ const backoffSeconds = 5;
556
+ const result = db.prepare(`UPDATE queue_messages
557
+ SET status = 'pending', delivery_id = NULL, visible_at = ?, updated_at = ?
558
+ WHERE id = ? AND delivery_id = ?`).run(now + backoffSeconds, now, messageId, deliveryId);
559
+ if (result.changes === 0) {
560
+ return c.json({ success: false, error: "Message not found or already processed" }, 404);
561
+ }
562
+ return c.json({ success: true });
563
+ } catch (err) {
564
+ const message = err instanceof Error ? err.message : String(err);
565
+ return c.json({ success: false, error: message }, 500);
566
+ }
567
+ };
568
+ return {
569
+ sendHandler,
570
+ batchSendHandler,
571
+ receiveHandler,
572
+ ackHandler,
573
+ retryHandler
574
+ };
575
+ }
576
+ function createWorkflowHandlers(db, workerUrl) {
577
+ const triggerHandler = async (c) => {
578
+ try {
579
+ const body = await c.req.json();
580
+ const { workflowName, input } = body;
581
+ const id = randomUUID();
582
+ const now = Math.floor(Date.now() / 1e3);
583
+ db.prepare(`INSERT INTO workflow_executions (id, workflow_name, status, input, started_at, created_at)
584
+ VALUES (?, ?, 'running', ?, ?, ?)`).run(id, workflowName, JSON.stringify(input), now, now);
585
+ if (workerUrl) {
586
+ fetch(workerUrl, {
587
+ method: "POST",
588
+ headers: {
589
+ "Content-Type": "application/json",
590
+ "X-Ploy-Workflow-Execution": "true",
591
+ "X-Ploy-Workflow-Name": workflowName,
592
+ "X-Ploy-Execution-Id": id
593
+ },
594
+ body: JSON.stringify(input)
595
+ }).catch(() => {
596
+ });
597
+ }
598
+ return c.json({ success: true, executionId: id });
599
+ } catch (err) {
600
+ const message = err instanceof Error ? err.message : String(err);
601
+ return c.json({ success: false, error: message }, 500);
602
+ }
603
+ };
604
+ const statusHandler = async (c) => {
605
+ try {
606
+ const body = await c.req.json();
607
+ const { executionId } = body;
608
+ const execution = db.prepare(`SELECT id, workflow_name, status, input, output, error, started_at, completed_at, created_at
609
+ FROM workflow_executions WHERE id = ?`).get(executionId);
610
+ if (!execution) {
611
+ return c.json({ success: false, error: "Execution not found" }, 404);
612
+ }
613
+ const steps = db.prepare(`SELECT step_name, step_index, status, output, error, duration_ms
614
+ FROM workflow_steps WHERE execution_id = ? ORDER BY step_index`).all(executionId);
615
+ return c.json({
616
+ success: true,
617
+ execution: {
618
+ id: execution.id,
619
+ workflowName: execution.workflow_name,
620
+ status: execution.status,
621
+ input: execution.input ? JSON.parse(execution.input) : null,
622
+ output: execution.output ? JSON.parse(execution.output) : null,
623
+ error: execution.error,
624
+ startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
625
+ completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
626
+ steps: steps.map((s) => ({
627
+ stepName: s.step_name,
628
+ stepIndex: s.step_index,
629
+ status: s.status,
630
+ output: s.output ? JSON.parse(s.output) : null,
631
+ error: s.error,
632
+ durationMs: s.duration_ms
633
+ }))
634
+ }
635
+ });
636
+ } catch (err) {
637
+ const message = err instanceof Error ? err.message : String(err);
638
+ return c.json({ success: false, error: message }, 500);
639
+ }
640
+ };
641
+ const cancelHandler = async (c) => {
642
+ try {
643
+ const body = await c.req.json();
644
+ const { executionId } = body;
645
+ const now = Math.floor(Date.now() / 1e3);
646
+ const result = db.prepare(`UPDATE workflow_executions
647
+ SET status = 'cancelled', completed_at = ?
648
+ WHERE id = ? AND status IN ('pending', 'running')`).run(now, executionId);
649
+ if (result.changes === 0) {
650
+ return c.json({ success: false, error: "Execution not found or already completed" }, 404);
651
+ }
652
+ return c.json({ success: true });
653
+ } catch (err) {
654
+ const message = err instanceof Error ? err.message : String(err);
655
+ return c.json({ success: false, error: message }, 500);
656
+ }
657
+ };
658
+ const stepStartHandler = async (c) => {
659
+ try {
660
+ const body = await c.req.json();
661
+ const { executionId, stepName, stepIndex } = body;
662
+ const existing = db.prepare(`SELECT status, output FROM workflow_steps
663
+ WHERE execution_id = ? AND step_name = ? AND step_index = ?`).get(executionId, stepName, stepIndex);
664
+ if (existing && existing.status === "completed") {
665
+ return c.json({
666
+ success: true,
667
+ alreadyCompleted: true,
668
+ output: existing.output ? JSON.parse(existing.output) : null
669
+ });
670
+ }
671
+ const now = Math.floor(Date.now() / 1e3);
672
+ if (existing) {
673
+ db.prepare(`UPDATE workflow_steps SET status = 'running' WHERE execution_id = ? AND step_name = ?`).run(executionId, stepName);
674
+ } else {
675
+ db.prepare(`INSERT INTO workflow_steps (execution_id, step_name, step_index, status, created_at)
676
+ VALUES (?, ?, ?, 'running', ?)`).run(executionId, stepName, stepIndex, now);
677
+ }
678
+ return c.json({ success: true, alreadyCompleted: false });
679
+ } catch (err) {
680
+ const message = err instanceof Error ? err.message : String(err);
681
+ return c.json({ success: false, error: message }, 500);
682
+ }
683
+ };
684
+ const stepCompleteHandler = async (c) => {
685
+ try {
686
+ const body = await c.req.json();
687
+ const { executionId, stepName, output, durationMs } = body;
688
+ db.prepare(`UPDATE workflow_steps
689
+ SET status = 'completed', output = ?, duration_ms = ?
690
+ WHERE execution_id = ? AND step_name = ?`).run(JSON.stringify(output), durationMs, executionId, stepName);
691
+ return c.json({ success: true });
692
+ } catch (err) {
693
+ const message = err instanceof Error ? err.message : String(err);
694
+ return c.json({ success: false, error: message }, 500);
695
+ }
696
+ };
697
+ const stepFailHandler = async (c) => {
698
+ try {
699
+ const body = await c.req.json();
700
+ const { executionId, stepName, error: error2, durationMs } = body;
701
+ db.prepare(`UPDATE workflow_steps
702
+ SET status = 'failed', error = ?, duration_ms = ?
703
+ WHERE execution_id = ? AND step_name = ?`).run(error2, durationMs, executionId, stepName);
704
+ return c.json({ success: true });
705
+ } catch (err) {
706
+ const message = err instanceof Error ? err.message : String(err);
707
+ return c.json({ success: false, error: message }, 500);
708
+ }
709
+ };
710
+ const completeHandler = async (c) => {
711
+ try {
712
+ const body = await c.req.json();
713
+ const { executionId, output } = body;
714
+ const now = Math.floor(Date.now() / 1e3);
715
+ db.prepare(`UPDATE workflow_executions
716
+ SET status = 'completed', output = ?, completed_at = ?
717
+ WHERE id = ?`).run(JSON.stringify(output), now, executionId);
718
+ return c.json({ success: true });
719
+ } catch (err) {
720
+ const message = err instanceof Error ? err.message : String(err);
721
+ return c.json({ success: false, error: message }, 500);
722
+ }
723
+ };
724
+ const failHandler = async (c) => {
725
+ try {
726
+ const body = await c.req.json();
727
+ const { executionId, error: error2 } = body;
728
+ const now = Math.floor(Date.now() / 1e3);
729
+ db.prepare(`UPDATE workflow_executions
730
+ SET status = 'failed', error = ?, completed_at = ?
731
+ WHERE id = ?`).run(error2, now, executionId);
732
+ return c.json({ success: true });
733
+ } catch (err) {
734
+ const message = err instanceof Error ? err.message : String(err);
735
+ return c.json({ success: false, error: message }, 500);
736
+ }
737
+ };
738
+ return {
739
+ triggerHandler,
740
+ statusHandler,
741
+ cancelHandler,
742
+ stepStartHandler,
743
+ stepCompleteHandler,
744
+ stepFailHandler,
745
+ completeHandler,
746
+ failHandler
747
+ };
748
+ }
749
+
750
+ // ../emulator/dist/services/mock-server.js
751
+ var DEFAULT_MOCK_SERVER_PORT = 4003;
752
+ async function startMockServer(dbManager2, config, options = {}) {
753
+ const app = new Hono();
754
+ if (config.db) {
755
+ const dbHandler = createDbHandler(dbManager2.getD1Database);
756
+ app.post("/db", dbHandler);
757
+ }
758
+ if (config.queue) {
759
+ const queueHandlers = createQueueHandlers(dbManager2.emulatorDb);
760
+ app.post("/queue/send", queueHandlers.sendHandler);
761
+ app.post("/queue/batch-send", queueHandlers.batchSendHandler);
762
+ app.post("/queue/receive", queueHandlers.receiveHandler);
763
+ app.post("/queue/ack", queueHandlers.ackHandler);
764
+ app.post("/queue/retry", queueHandlers.retryHandler);
765
+ }
766
+ if (config.workflow) {
767
+ const workflowHandlers = createWorkflowHandlers(dbManager2.emulatorDb, options.workerUrl);
768
+ app.post("/workflow/trigger", workflowHandlers.triggerHandler);
769
+ app.post("/workflow/status", workflowHandlers.statusHandler);
770
+ app.post("/workflow/cancel", workflowHandlers.cancelHandler);
771
+ app.post("/workflow/step/start", workflowHandlers.stepStartHandler);
772
+ app.post("/workflow/step/complete", workflowHandlers.stepCompleteHandler);
773
+ app.post("/workflow/step/fail", workflowHandlers.stepFailHandler);
774
+ app.post("/workflow/complete", workflowHandlers.completeHandler);
775
+ app.post("/workflow/fail", workflowHandlers.failHandler);
776
+ }
777
+ app.get("/health", (c) => c.json({ status: "ok" }));
778
+ if (options.dashboardEnabled !== false) {
779
+ createDashboardRoutes(app, dbManager2, config);
780
+ }
781
+ const serverPort = options.port ?? DEFAULT_MOCK_SERVER_PORT;
782
+ return await new Promise((resolve) => {
783
+ const server = serve({
784
+ fetch: app.fetch,
785
+ port: serverPort
786
+ }, (info) => {
787
+ resolve({
788
+ port: info.port,
789
+ close: () => new Promise((res) => {
790
+ server.close(() => res());
791
+ })
792
+ });
793
+ });
794
+ });
795
+ }
796
+ function getDataDir(projectDir) {
797
+ return join(projectDir, ".ploy");
798
+ }
799
+ function ensureDir(dir) {
800
+ mkdirSync(dir, { recursive: true });
801
+ }
802
+ function ensureDataDir(projectDir) {
803
+ const dataDir = getDataDir(projectDir);
804
+ ensureDir(dataDir);
805
+ ensureDir(join(dataDir, "db"));
806
+ return dataDir;
807
+ }
808
+ var EMULATOR_SCHEMA = `
809
+ -- Queue messages table
810
+ CREATE TABLE IF NOT EXISTS queue_messages (
811
+ id TEXT PRIMARY KEY,
812
+ queue_name TEXT NOT NULL,
813
+ payload TEXT NOT NULL,
814
+ status TEXT DEFAULT 'pending',
815
+ attempt INTEGER DEFAULT 0,
816
+ delivery_id TEXT,
817
+ visible_at INTEGER NOT NULL,
818
+ created_at INTEGER DEFAULT (strftime('%s', 'now')),
819
+ updated_at INTEGER DEFAULT (strftime('%s', 'now'))
820
+ );
821
+
822
+ CREATE INDEX IF NOT EXISTS idx_queue_messages_status
823
+ ON queue_messages(queue_name, status, visible_at);
824
+
825
+ -- Workflow executions table
826
+ CREATE TABLE IF NOT EXISTS workflow_executions (
827
+ id TEXT PRIMARY KEY,
828
+ workflow_name TEXT NOT NULL,
829
+ status TEXT DEFAULT 'pending',
830
+ input TEXT,
831
+ output TEXT,
832
+ error TEXT,
833
+ started_at INTEGER,
834
+ completed_at INTEGER,
835
+ created_at INTEGER DEFAULT (strftime('%s', 'now'))
836
+ );
837
+
838
+ CREATE INDEX IF NOT EXISTS idx_workflow_executions_status
839
+ ON workflow_executions(workflow_name, status);
840
+
841
+ -- Workflow steps table
842
+ CREATE TABLE IF NOT EXISTS workflow_steps (
843
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
844
+ execution_id TEXT NOT NULL,
845
+ step_name TEXT NOT NULL,
846
+ step_index INTEGER NOT NULL,
847
+ status TEXT DEFAULT 'pending',
848
+ output TEXT,
849
+ error TEXT,
850
+ duration_ms INTEGER,
851
+ created_at INTEGER DEFAULT (strftime('%s', 'now')),
852
+ FOREIGN KEY (execution_id) REFERENCES workflow_executions(id)
853
+ );
854
+
855
+ CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
856
+ ON workflow_steps(execution_id, step_index);
857
+ `;
858
+ function initializeDatabases(projectDir) {
859
+ const dataDir = ensureDataDir(projectDir);
860
+ const d1Databases = /* @__PURE__ */ new Map();
861
+ const emulatorDb = new Database(join(dataDir, "emulator.db"));
862
+ emulatorDb.pragma("journal_mode = WAL");
863
+ emulatorDb.exec(EMULATOR_SCHEMA);
864
+ function getD1Database(bindingName) {
865
+ let db = d1Databases.get(bindingName);
866
+ if (!db) {
867
+ db = new Database(join(dataDir, "db", `${bindingName}.db`));
868
+ db.pragma("journal_mode = WAL");
869
+ d1Databases.set(bindingName, db);
870
+ }
871
+ return db;
872
+ }
873
+ function close() {
874
+ emulatorDb.close();
875
+ for (const db of d1Databases.values()) {
876
+ db.close();
877
+ }
878
+ d1Databases.clear();
879
+ }
880
+ return {
881
+ emulatorDb,
882
+ getD1Database,
883
+ close
884
+ };
885
+ }
886
+
887
+ // ../emulator/dist/nextjs-dev.js
888
+ var PLOY_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("__ploy-context__");
889
+ function createDevD1(databaseId, apiUrl) {
890
+ return {
891
+ async dump() {
892
+ const response = await fetch(`${apiUrl}/db`, {
893
+ method: "POST",
894
+ headers: { "Content-Type": "application/json" },
895
+ body: JSON.stringify({
896
+ databaseId,
897
+ method: "dump"
898
+ })
899
+ });
900
+ if (!response.ok) {
901
+ throw new Error(`DB dump failed: ${await response.text()}`);
902
+ }
903
+ return await response.arrayBuffer();
904
+ },
905
+ prepare(query) {
906
+ let boundParams = [];
907
+ const stmt = {
908
+ bind(...values) {
909
+ boundParams = values;
910
+ return stmt;
911
+ },
912
+ async run() {
913
+ const response = await fetch(`${apiUrl}/db`, {
914
+ method: "POST",
915
+ headers: { "Content-Type": "application/json" },
916
+ body: JSON.stringify({
917
+ databaseId,
918
+ method: "prepare",
919
+ query,
920
+ params: boundParams
921
+ })
922
+ });
923
+ if (!response.ok) {
924
+ throw new Error(`DB query failed: ${await response.text()}`);
925
+ }
926
+ return await response.json();
927
+ },
928
+ async all() {
929
+ return await stmt.run();
930
+ },
931
+ async first(colName) {
932
+ const result = await stmt.run();
933
+ if (result.results.length === 0) {
934
+ return null;
935
+ }
936
+ const firstRow = result.results[0];
937
+ if (colName) {
938
+ return firstRow[colName] ?? null;
939
+ }
940
+ return firstRow;
941
+ },
942
+ async raw() {
943
+ const result = await stmt.run();
944
+ if (result.results.length === 0) {
945
+ return [];
946
+ }
947
+ const keys = Object.keys(result.results[0]);
948
+ return result.results.map((row) => keys.map((k) => row[k]));
949
+ }
950
+ };
951
+ return stmt;
952
+ },
953
+ async exec(query) {
954
+ const response = await fetch(`${apiUrl}/db`, {
955
+ method: "POST",
956
+ headers: { "Content-Type": "application/json" },
957
+ body: JSON.stringify({
958
+ databaseId,
959
+ method: "exec",
960
+ query
961
+ })
962
+ });
963
+ if (!response.ok) {
964
+ throw new Error(`DB exec failed: ${await response.text()}`);
965
+ }
966
+ return await response.json();
967
+ },
968
+ async batch(statements) {
969
+ const stmts = statements.map((s) => {
970
+ const data = s.__db_data;
971
+ return data || s;
972
+ });
973
+ const response = await fetch(`${apiUrl}/db`, {
974
+ method: "POST",
975
+ headers: { "Content-Type": "application/json" },
976
+ body: JSON.stringify({
977
+ databaseId,
978
+ method: "batch",
979
+ statements: stmts
980
+ })
981
+ });
982
+ if (!response.ok) {
983
+ throw new Error(`DB batch failed: ${await response.text()}`);
984
+ }
985
+ return await response.json();
986
+ }
987
+ };
988
+ }
989
+ var mockServer = null;
990
+ var dbManager = null;
991
+ async function initPloyForDev(config) {
992
+ if (process.env.NODE_ENV !== "development") {
993
+ return;
994
+ }
995
+ if (globalThis.__PLOY_DEV_INITIALIZED__) {
996
+ return;
997
+ }
998
+ globalThis.__PLOY_DEV_INITIALIZED__ = true;
999
+ const configPath = config?.configPath || "./ploy.yaml";
1000
+ const projectDir = process.cwd();
1001
+ let ployConfig;
1002
+ try {
1003
+ ployConfig = readPloyConfig(projectDir, configPath);
1004
+ } catch {
1005
+ if (config?.bindings?.db) {
1006
+ ployConfig = { db: config.bindings.db };
1007
+ } else {
1008
+ return;
1009
+ }
1010
+ }
1011
+ if (config?.bindings?.db) {
1012
+ ployConfig = { ...ployConfig, db: config.bindings.db };
1013
+ }
1014
+ if (!ployConfig.db || Object.keys(ployConfig.db).length === 0) {
1015
+ return;
1016
+ }
1017
+ ensureDataDir(projectDir);
1018
+ dbManager = initializeDatabases(projectDir);
1019
+ mockServer = await startMockServer(dbManager, ployConfig, {});
1020
+ const apiUrl = `http://localhost:${mockServer.port}`;
1021
+ const env = {};
1022
+ for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
1023
+ env[bindingName] = createDevD1(databaseId, apiUrl);
1024
+ }
1025
+ const context = {
1026
+ env,
1027
+ cf: void 0,
1028
+ ctx: void 0
1029
+ };
1030
+ globalThis.__PLOY_DEV_CONTEXT__ = context;
1031
+ Object.defineProperty(globalThis, PLOY_CONTEXT_SYMBOL, {
1032
+ get() {
1033
+ return context;
1034
+ },
1035
+ configurable: true
1036
+ });
1037
+ const bindingNames = Object.keys(env);
1038
+ console.log(`[Ploy] Development context initialized with bindings: ${bindingNames.join(", ")}`);
1039
+ console.log(`[Ploy] Mock server running at ${apiUrl}`);
1040
+ const cleanup = async () => {
1041
+ if (mockServer) {
1042
+ await mockServer.close();
1043
+ mockServer = null;
1044
+ }
1045
+ if (dbManager) {
1046
+ dbManager.close();
1047
+ dbManager = null;
1048
+ }
1049
+ };
1050
+ process.on("exit", () => {
1051
+ if (dbManager) {
1052
+ dbManager.close();
1053
+ }
1054
+ });
1055
+ process.on("SIGINT", async () => {
1056
+ await cleanup();
1057
+ process.exit(0);
1058
+ });
1059
+ process.on("SIGTERM", async () => {
1060
+ await cleanup();
1061
+ process.exit(0);
1062
+ });
1063
+ }
1064
+
1065
+ export { initPloyForDev };