@meetploy/cli 1.20.0 → 1.21.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/index.js CHANGED
@@ -118,6 +118,15 @@ var init_package_manager = __esm({
118
118
  "../tools/dist/package-manager.js"() {
119
119
  }
120
120
  });
121
+ function getWorkflowName(value) {
122
+ return typeof value === "string" ? value : value.name;
123
+ }
124
+ function getWorkflowTimeout(value) {
125
+ return typeof value === "string" ? void 0 : value.timeout;
126
+ }
127
+ function getWorkflowStepTimeout(value) {
128
+ return typeof value === "string" ? void 0 : value.step_timeout;
129
+ }
121
130
  function getCompatibilityFlags(config) {
122
131
  if (!config.compatibility_flags || config.compatibility_flags.length === 0) {
123
132
  return DEFAULT_COMPATIBILITY_FLAGS;
@@ -163,6 +172,47 @@ function validateCronBindings(bindings, configFile) {
163
172
  }
164
173
  }
165
174
  }
175
+ function validateWorkflowBindings(bindings, configFile) {
176
+ if (bindings === void 0) {
177
+ return;
178
+ }
179
+ for (const [bindingName, value] of Object.entries(bindings)) {
180
+ if (!BINDING_NAME_REGEX.test(bindingName)) {
181
+ throw new Error(`Invalid workflow binding name '${bindingName}' in ${configFile}. Binding names must be uppercase with underscores (e.g., ORDER_FLOW)`);
182
+ }
183
+ if (typeof value === "string") {
184
+ if (!RESOURCE_NAME_REGEX.test(value)) {
185
+ throw new Error(`Invalid workflow resource name '${value}' for binding '${bindingName}' in ${configFile}. Resource names must be lowercase with underscores (e.g., order_processing)`);
186
+ }
187
+ } else if (typeof value === "object" && value !== null) {
188
+ if (typeof value.name !== "string") {
189
+ throw new Error(`Workflow binding '${bindingName}' in ${configFile} must have a 'name' field`);
190
+ }
191
+ if (!RESOURCE_NAME_REGEX.test(value.name)) {
192
+ throw new Error(`Invalid workflow resource name '${value.name}' for binding '${bindingName}' in ${configFile}. Resource names must be lowercase with underscores (e.g., order_processing)`);
193
+ }
194
+ if (value.timeout !== void 0 && (typeof value.timeout !== "number" || !Number.isFinite(value.timeout) || value.timeout <= 0)) {
195
+ throw new Error(`'timeout' for workflow binding '${bindingName}' in ${configFile} must be a positive number (seconds)`);
196
+ }
197
+ if (value.timeout !== void 0 && value.timeout > MAX_WORKFLOW_TIMEOUT_S) {
198
+ throw new Error(`'timeout' for workflow binding '${bindingName}' in ${configFile} exceeds maximum of ${MAX_WORKFLOW_TIMEOUT_S} seconds (60 minutes)`);
199
+ }
200
+ if (value.step_timeout !== void 0) {
201
+ if (typeof value.step_timeout !== "number" || !Number.isFinite(value.step_timeout) || value.step_timeout <= 0) {
202
+ throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} must be a positive number (seconds)`);
203
+ }
204
+ if (value.step_timeout > MAX_WORKFLOW_TIMEOUT_S) {
205
+ throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} exceeds maximum of ${MAX_WORKFLOW_TIMEOUT_S} seconds (60 minutes)`);
206
+ }
207
+ if (value.timeout !== void 0 && value.step_timeout > value.timeout) {
208
+ throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} cannot exceed its 'timeout' of ${value.timeout} seconds`);
209
+ }
210
+ }
211
+ } else {
212
+ throw new Error(`Workflow binding '${bindingName}' in ${configFile} must be a string or object with 'name'`);
213
+ }
214
+ }
215
+ }
166
216
  function validateRelativePath(path, fieldName, configFile) {
167
217
  if (path === void 0) {
168
218
  return void 0;
@@ -217,7 +267,7 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
217
267
  validateBindings(config.cache, "cache", configFile);
218
268
  validateBindings(config.state, "state", configFile);
219
269
  validateBindings(config.fs, "fs", configFile);
220
- validateBindings(config.workflow, "workflow", configFile);
270
+ validateWorkflowBindings(config.workflow, configFile);
221
271
  validateCronBindings(config.cron, configFile);
222
272
  validateBindings(config.timer, "timer", configFile);
223
273
  if (config.ai !== void 0 && typeof config.ai !== "boolean") {
@@ -357,10 +407,13 @@ function getWorkerEntryPoint(projectDir, config) {
357
407
  }
358
408
  throw new Error("Could not find worker entry point. Specify 'main' in ploy.yaml");
359
409
  }
360
- var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
410
+ var readFileAsync, MAX_WORKFLOW_TIMEOUT_S, DEFAULT_WORKFLOW_TIMEOUT_S, DEFAULT_STEP_TIMEOUT_S, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
361
411
  var init_ploy_config = __esm({
362
412
  "../tools/dist/ploy-config.js"() {
363
413
  readFileAsync = promisify(readFile$1);
414
+ MAX_WORKFLOW_TIMEOUT_S = 60 * 60;
415
+ DEFAULT_WORKFLOW_TIMEOUT_S = 10 * 60;
416
+ DEFAULT_STEP_TIMEOUT_S = 2 * 60;
364
417
  DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat_v2"];
365
418
  DEFAULT_COMPATIBILITY_DATE = "2025-04-02";
366
419
  BINDING_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
@@ -374,12 +427,18 @@ var cli_exports = {};
374
427
  __export(cli_exports, {
375
428
  DEFAULT_COMPATIBILITY_DATE: () => DEFAULT_COMPATIBILITY_DATE,
376
429
  DEFAULT_COMPATIBILITY_FLAGS: () => DEFAULT_COMPATIBILITY_FLAGS,
430
+ DEFAULT_STEP_TIMEOUT_S: () => DEFAULT_STEP_TIMEOUT_S,
431
+ DEFAULT_WORKFLOW_TIMEOUT_S: () => DEFAULT_WORKFLOW_TIMEOUT_S,
432
+ MAX_WORKFLOW_TIMEOUT_S: () => MAX_WORKFLOW_TIMEOUT_S,
377
433
  detectMonorepo: () => detectMonorepo,
378
434
  getAddDevDependencyCommand: () => getAddDevDependencyCommand,
379
435
  getCompatibilityDate: () => getCompatibilityDate,
380
436
  getCompatibilityFlags: () => getCompatibilityFlags,
381
437
  getRunCommand: () => getRunCommand,
382
438
  getWorkerEntryPoint: () => getWorkerEntryPoint,
439
+ getWorkflowName: () => getWorkflowName,
440
+ getWorkflowStepTimeout: () => getWorkflowStepTimeout,
441
+ getWorkflowTimeout: () => getWorkflowTimeout,
383
442
  hasBindings: () => hasBindings,
384
443
  isPnpmWorkspace: () => isPnpmWorkspace,
385
444
  loadDotEnvSync: () => loadDotEnvSync,
@@ -398,6 +457,26 @@ var init_cli = __esm({
398
457
  }
399
458
  });
400
459
 
460
+ // ../emulator/dist/config/ploy-config.js
461
+ function readPloyConfig2(projectDir, configPath) {
462
+ const config = readPloyConfigSync(projectDir, configPath);
463
+ if (!config.kind) {
464
+ throw new Error(`Missing required field 'kind' in ${configPath ?? "ploy.yaml"}`);
465
+ }
466
+ if (config.kind !== "dynamic" && config.kind !== "worker") {
467
+ throw new Error(`Invalid kind '${config.kind}' in ${configPath ?? "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
468
+ }
469
+ return config;
470
+ }
471
+ function getWorkerEntryPoint2(projectDir, config) {
472
+ return getWorkerEntryPoint(projectDir, config);
473
+ }
474
+ var init_ploy_config2 = __esm({
475
+ "../emulator/dist/config/ploy-config.js"() {
476
+ init_cli();
477
+ }
478
+ });
479
+
401
480
  // ../emulator/dist/runtime/cache-runtime.js
402
481
  var CACHE_RUNTIME_CODE;
403
482
  var init_cache_runtime = __esm({
@@ -458,10 +537,10 @@ export function initializeCache(cacheName: string, serviceUrl: string): CacheBin
458
537
  }
459
538
  });
460
539
 
461
- // ../shared/dist/d1-runtime.js
540
+ // ../shared/dist/db-runtime.js
462
541
  var DB_RUNTIME_CODE, DB_RUNTIME_CODE_PRODUCTION;
463
- var init_d1_runtime = __esm({
464
- "../shared/dist/d1-runtime.js"() {
542
+ var init_db_runtime = __esm({
543
+ "../shared/dist/db-runtime.js"() {
465
544
  DB_RUNTIME_CODE = `
466
545
  interface DBResult {
467
546
  results: unknown[];
@@ -481,14 +560,14 @@ interface DBPreparedStatement {
481
560
  raw: (options?: { columnNames?: boolean }) => Promise<unknown[][]>;
482
561
  }
483
562
 
484
- interface DBDatabase {
563
+ interface Database {
485
564
  prepare: (query: string) => DBPreparedStatement;
486
565
  dump: () => Promise<ArrayBuffer>;
487
566
  batch: (statements: DBPreparedStatement[]) => Promise<DBResult[]>;
488
567
  exec: (query: string) => Promise<DBResult>;
489
568
  }
490
569
 
491
- export function initializeDB(bindingName: string, serviceUrl: string): DBDatabase {
570
+ export function initializeDB(bindingName: string, serviceUrl: string): Database {
492
571
  return {
493
572
  prepare(query: string): DBPreparedStatement {
494
573
  let boundParams: unknown[] = [];
@@ -760,7 +839,7 @@ var init_url_validation = __esm({
760
839
  // ../shared/dist/index.js
761
840
  var init_dist = __esm({
762
841
  "../shared/dist/index.js"() {
763
- init_d1_runtime();
842
+ init_db_runtime();
764
843
  init_error();
765
844
  init_health_check();
766
845
  init_trace_event();
@@ -771,7 +850,7 @@ var init_dist = __esm({
771
850
  });
772
851
 
773
852
  // ../emulator/dist/runtime/db-runtime.js
774
- var init_db_runtime = __esm({
853
+ var init_db_runtime2 = __esm({
775
854
  "../emulator/dist/runtime/db-runtime.js"() {
776
855
  init_dist();
777
856
  }
@@ -1333,7 +1412,8 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
1333
1412
  }
1334
1413
  if (config.workflow) {
1335
1414
  imports.push('import { initializeWorkflow, createStepContext, executeWorkflow } from "__ploy_workflow_runtime__";');
1336
- for (const [bindingName, workflowName] of Object.entries(config.workflow)) {
1415
+ for (const [bindingName, value] of Object.entries(config.workflow)) {
1416
+ const workflowName = getWorkflowName(value);
1337
1417
  bindings.push(` ${bindingName}: initializeWorkflow("${workflowName}", "${mockServiceUrl}"),`);
1338
1418
  }
1339
1419
  }
@@ -1608,8 +1688,9 @@ async function bundleWorker(options) {
1608
1688
  var NODE_BUILTINS;
1609
1689
  var init_bundler = __esm({
1610
1690
  "../emulator/dist/bundler/bundler.js"() {
1691
+ init_ploy_config2();
1611
1692
  init_cache_runtime();
1612
- init_db_runtime();
1693
+ init_db_runtime2();
1613
1694
  init_fs_runtime();
1614
1695
  init_queue_runtime();
1615
1696
  init_state_runtime();
@@ -1671,6 +1752,9 @@ function log(message) {
1671
1752
  function success(message) {
1672
1753
  console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.green}[ploy]${COLORS.reset} ${message}`);
1673
1754
  }
1755
+ function warn(message) {
1756
+ console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.yellow}[ploy]${COLORS.reset} ${message}`);
1757
+ }
1674
1758
  function error(message) {
1675
1759
  console.error(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.red}[ploy]${COLORS.reset} ${message}`);
1676
1760
  }
@@ -1802,26 +1886,6 @@ var init_env = __esm({
1802
1886
  init_cli();
1803
1887
  }
1804
1888
  });
1805
-
1806
- // ../emulator/dist/config/ploy-config.js
1807
- function readPloyConfig2(projectDir, configPath) {
1808
- const config = readPloyConfigSync(projectDir, configPath);
1809
- if (!config.kind) {
1810
- throw new Error(`Missing required field 'kind' in ${configPath ?? "ploy.yaml"}`);
1811
- }
1812
- if (config.kind !== "dynamic" && config.kind !== "worker") {
1813
- throw new Error(`Invalid kind '${config.kind}' in ${configPath ?? "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
1814
- }
1815
- return config;
1816
- }
1817
- function getWorkerEntryPoint2(projectDir, config) {
1818
- return getWorkerEntryPoint(projectDir, config);
1819
- }
1820
- var init_ploy_config2 = __esm({
1821
- "../emulator/dist/config/ploy-config.js"() {
1822
- init_cli();
1823
- }
1824
- });
1825
1889
  function generateWorkerdConfig(options) {
1826
1890
  const { port, mockServicePort } = options;
1827
1891
  const services = [
@@ -2566,7 +2630,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2566
2630
  return c.json({ error: "Query is required" }, 400);
2567
2631
  }
2568
2632
  try {
2569
- const db = dbManager2.getD1Database(resourceName);
2633
+ const db = dbManager2.getDatabase(resourceName);
2570
2634
  const startTime = Date.now();
2571
2635
  const stmt = db.prepare(query);
2572
2636
  const isSelect = query.trim().toUpperCase().startsWith("SELECT");
@@ -2604,7 +2668,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2604
2668
  return c.json({ error: `Database binding '${binding}' not found` }, 404);
2605
2669
  }
2606
2670
  try {
2607
- const db = dbManager2.getD1Database(resourceName);
2671
+ const db = dbManager2.getDatabase(resourceName);
2608
2672
  const tables = db.prepare(`SELECT name FROM sqlite_master
2609
2673
  WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%'
2610
2674
  ORDER BY name`).all();
@@ -2623,7 +2687,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2623
2687
  const limit = parseInt(c.req.query("limit") ?? "50", 10);
2624
2688
  const offset = parseInt(c.req.query("offset") ?? "0", 10);
2625
2689
  try {
2626
- const db = dbManager2.getD1Database(resourceName);
2690
+ const db = dbManager2.getDatabase(resourceName);
2627
2691
  const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
2628
2692
  const columns = columnsResult.map((col) => col.name);
2629
2693
  const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
@@ -2641,7 +2705,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2641
2705
  return c.json({ error: `Database binding '${binding}' not found` }, 404);
2642
2706
  }
2643
2707
  try {
2644
- const db = dbManager2.getD1Database(resourceName);
2708
+ const db = dbManager2.getDatabase(resourceName);
2645
2709
  const tablesResult = db.prepare(`SELECT name FROM sqlite_master
2646
2710
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
2647
2711
  ORDER BY name`).all();
@@ -2794,13 +2858,50 @@ function createDashboardRoutes(app, dbManager2, config) {
2794
2858
  app.get("/api/workflow/:binding/executions", (c) => {
2795
2859
  const binding = c.req.param("binding");
2796
2860
  const workflowConfig = config.workflow?.[binding];
2797
- const limit = parseInt(c.req.query("limit") ?? "20", 10);
2861
+ const limit = Math.min(Math.max(1, parseInt(c.req.query("limit") ?? "20", 10)), 100);
2862
+ const offset = Math.max(0, parseInt(c.req.query("offset") ?? "0", 10));
2863
+ const statusFilter = c.req.query("status")?.toLowerCase() ?? null;
2864
+ const sinceRaw = c.req.query("since") ?? null;
2798
2865
  if (!workflowConfig) {
2799
2866
  return c.json({ error: "Workflow not found" }, 404);
2800
2867
  }
2868
+ const validStatuses = /* @__PURE__ */ new Set([
2869
+ "pending",
2870
+ "running",
2871
+ "completed",
2872
+ "failed",
2873
+ "cancelled"
2874
+ ]);
2801
2875
  try {
2802
2876
  const db = dbManager2.emulatorDb;
2803
- const workflowName = workflowConfig;
2877
+ const workflowName = getWorkflowName(workflowConfig);
2878
+ let sinceTs = null;
2879
+ if (sinceRaw) {
2880
+ const match = sinceRaw.match(/^(\d+)(s|m|h|d)?$/);
2881
+ if (match) {
2882
+ const val = parseInt(match[1], 10);
2883
+ const unit = match[2] ?? "s";
2884
+ const multipliers = {
2885
+ s: 1,
2886
+ m: 60,
2887
+ h: 3600,
2888
+ d: 86400
2889
+ };
2890
+ sinceTs = Math.floor(Date.now() / 1e3) - val * (multipliers[unit] ?? 1);
2891
+ }
2892
+ }
2893
+ const conditions = ["e.workflow_name = ?"];
2894
+ const queryArgs = [workflowName];
2895
+ if (statusFilter && validStatuses.has(statusFilter)) {
2896
+ conditions.push("e.status = ?");
2897
+ queryArgs.push(statusFilter);
2898
+ }
2899
+ if (sinceTs !== null) {
2900
+ conditions.push("e.created_at >= ?");
2901
+ queryArgs.push(sinceTs);
2902
+ }
2903
+ const where = conditions.join(" AND ");
2904
+ const countRow = db.prepare(`SELECT COUNT(*) as count FROM workflow_executions e WHERE ${where}`).get(...queryArgs);
2804
2905
  const executions = db.prepare(`SELECT
2805
2906
  e.id,
2806
2907
  e.workflow_name,
@@ -2812,9 +2913,9 @@ function createDashboardRoutes(app, dbManager2, config) {
2812
2913
  (SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id) as steps_count,
2813
2914
  (SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id AND status = 'completed') as steps_completed
2814
2915
  FROM workflow_executions e
2815
- WHERE e.workflow_name = ?
2916
+ WHERE ${where}
2816
2917
  ORDER BY e.created_at DESC
2817
- LIMIT ?`).all(workflowName, limit);
2918
+ LIMIT ? OFFSET ?`).all(...queryArgs, limit, offset);
2818
2919
  return c.json({
2819
2920
  executions: executions.map((e) => ({
2820
2921
  id: e.id,
@@ -2826,7 +2927,10 @@ function createDashboardRoutes(app, dbManager2, config) {
2826
2927
  stepsCompleted: e.steps_completed,
2827
2928
  errorMessage: e.error,
2828
2929
  createdAt: new Date(e.created_at * 1e3).toISOString()
2829
- }))
2930
+ })),
2931
+ total: countRow.count,
2932
+ limit,
2933
+ offset
2830
2934
  });
2831
2935
  } catch (err) {
2832
2936
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
@@ -3060,6 +3164,7 @@ function createDashboardRoutes(app, dbManager2, config) {
3060
3164
  var __filename, __dirname, MIME_TYPES;
3061
3165
  var init_dashboard_routes = __esm({
3062
3166
  "../emulator/dist/services/dashboard-routes.js"() {
3167
+ init_ploy_config2();
3063
3168
  __filename = fileURLToPath(import.meta.url);
3064
3169
  __dirname = dirname(__filename);
3065
3170
  MIME_TYPES = {
@@ -3742,7 +3847,7 @@ function createWorkflowHandlers(db, workerUrl) {
3742
3847
  }
3743
3848
  const now = Math.floor(Date.now() / 1e3);
3744
3849
  if (existing) {
3745
- db.prepare(`UPDATE workflow_steps SET status = 'running' WHERE execution_id = ? AND step_name = ?`).run(executionId, stepName);
3850
+ db.prepare(`UPDATE workflow_steps SET status = 'running', error = NULL, created_at = ? WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(now, executionId, stepName, stepIndex);
3746
3851
  } else {
3747
3852
  db.prepare(`INSERT INTO workflow_steps (execution_id, step_name, step_index, status, created_at)
3748
3853
  VALUES (?, ?, ?, 'running', ?)`).run(executionId, stepName, stepIndex, now);
@@ -3756,10 +3861,16 @@ function createWorkflowHandlers(db, workerUrl) {
3756
3861
  const stepCompleteHandler = async (c) => {
3757
3862
  try {
3758
3863
  const body = await c.req.json();
3759
- const { executionId, stepName, output, durationMs } = body;
3760
- db.prepare(`UPDATE workflow_steps
3761
- SET status = 'completed', output = ?, duration_ms = ?
3762
- WHERE execution_id = ? AND step_name = ?`).run(JSON.stringify(output), durationMs, executionId, stepName);
3864
+ const { executionId, stepName, stepIndex, output, durationMs } = body;
3865
+ if (stepIndex !== void 0) {
3866
+ db.prepare(`UPDATE workflow_steps
3867
+ SET status = 'completed', output = ?, duration_ms = ?
3868
+ WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(JSON.stringify(output), durationMs, executionId, stepName, stepIndex);
3869
+ } else {
3870
+ db.prepare(`UPDATE workflow_steps
3871
+ SET status = 'completed', output = ?, duration_ms = ?
3872
+ WHERE execution_id = ? AND step_name = ?`).run(JSON.stringify(output), durationMs, executionId, stepName);
3873
+ }
3763
3874
  return c.json({ success: true });
3764
3875
  } catch (err) {
3765
3876
  const message = err instanceof Error ? err.message : String(err);
@@ -3769,10 +3880,16 @@ function createWorkflowHandlers(db, workerUrl) {
3769
3880
  const stepFailHandler = async (c) => {
3770
3881
  try {
3771
3882
  const body = await c.req.json();
3772
- const { executionId, stepName, error: error2, durationMs } = body;
3773
- db.prepare(`UPDATE workflow_steps
3774
- SET status = 'failed', error = ?, duration_ms = ?
3775
- WHERE execution_id = ? AND step_name = ?`).run(error2, durationMs, executionId, stepName);
3883
+ const { executionId, stepName, stepIndex, error: error2, durationMs } = body;
3884
+ if (stepIndex !== void 0) {
3885
+ db.prepare(`UPDATE workflow_steps
3886
+ SET status = 'failed', error = ?, duration_ms = ?
3887
+ WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(error2, durationMs, executionId, stepName, stepIndex);
3888
+ } else {
3889
+ db.prepare(`UPDATE workflow_steps
3890
+ SET status = 'failed', error = ?, duration_ms = ?
3891
+ WHERE execution_id = ? AND step_name = ?`).run(error2, durationMs, executionId, stepName);
3892
+ }
3776
3893
  return c.json({ success: true });
3777
3894
  } catch (err) {
3778
3895
  const message = err instanceof Error ? err.message : String(err);
@@ -3818,14 +3935,124 @@ function createWorkflowHandlers(db, workerUrl) {
3818
3935
  failHandler
3819
3936
  };
3820
3937
  }
3938
+ function startWorkflowTimeoutEnforcement(db, intervalMs = 5e3, timeouts) {
3939
+ let isRunning = false;
3940
+ return setInterval(() => void (async () => {
3941
+ if (isRunning) {
3942
+ return;
3943
+ }
3944
+ isRunning = true;
3945
+ try {
3946
+ const now = Math.floor(Date.now() / 1e3);
3947
+ const allTimeouts = [
3948
+ timeouts.defaultTimeoutS,
3949
+ ...Object.values(timeouts.perWorkflow)
3950
+ ];
3951
+ const minTimeoutS = Math.min(...allTimeouts);
3952
+ const queryThreshold = now - minTimeoutS;
3953
+ const timedOutCandidates = db.prepare(`SELECT id, workflow_name, started_at FROM workflow_executions
3954
+ WHERE status = 'running' AND started_at < ?`).all(queryThreshold);
3955
+ for (const execution of timedOutCandidates) {
3956
+ const timeoutS = timeouts.perWorkflow[execution.workflow_name] ?? timeouts.defaultTimeoutS;
3957
+ const threshold = now - timeoutS;
3958
+ if (execution.started_at >= threshold) {
3959
+ continue;
3960
+ }
3961
+ db.prepare(`UPDATE workflow_executions
3962
+ SET status = 'failed', error = ?, completed_at = ?
3963
+ WHERE id = ? AND status = 'running'`).run(`Workflow exceeded timeout of ${timeoutS}s`, now, execution.id);
3964
+ }
3965
+ const allStepTimeouts = [
3966
+ timeouts.defaultStepTimeoutS,
3967
+ ...Object.values(timeouts.perWorkflowStep)
3968
+ ];
3969
+ const minStepTimeoutS = Math.min(...allStepTimeouts);
3970
+ const stepQueryThreshold = now - minStepTimeoutS;
3971
+ const timedOutSteps = db.prepare(`SELECT ws.id, ws.execution_id, ws.step_name, ws.step_index, ws.created_at, we.workflow_name
3972
+ FROM workflow_steps ws
3973
+ JOIN workflow_executions we ON we.id = ws.execution_id
3974
+ WHERE ws.status = 'running' AND ws.created_at < ?`).all(stepQueryThreshold);
3975
+ for (const step of timedOutSteps) {
3976
+ const stepTimeoutS = timeouts.perWorkflowStep[step.workflow_name] ?? timeouts.defaultStepTimeoutS;
3977
+ const stepThreshold = now - stepTimeoutS;
3978
+ if (step.created_at >= stepThreshold) {
3979
+ continue;
3980
+ }
3981
+ db.transaction(() => {
3982
+ db.prepare(`UPDATE workflow_steps
3983
+ SET status = 'failed', error = ?
3984
+ WHERE id = ? AND status = 'running'`).run(`Step exceeded timeout of ${stepTimeoutS}s`, step.id);
3985
+ db.prepare(`UPDATE workflow_executions
3986
+ SET status = 'failed', error = ?, completed_at = ?
3987
+ WHERE id = ? AND status = 'running'`).run(`Step '${step.step_name}' exceeded timeout of ${stepTimeoutS}s`, now, step.execution_id);
3988
+ })();
3989
+ }
3990
+ } catch (err) {
3991
+ warn(`Failed to enforce workflow timeouts: ${err instanceof Error ? err.message : String(err)}`);
3992
+ } finally {
3993
+ isRunning = false;
3994
+ }
3995
+ })(), intervalMs);
3996
+ }
3997
+ function startStalledWorkflowRecovery(db, workerUrl, intervalMs = 5e3, stallTimeoutS = DEFAULT_STALL_TIMEOUT_S) {
3998
+ let isRunning = false;
3999
+ return setInterval(() => void (async () => {
4000
+ if (isRunning) {
4001
+ return;
4002
+ }
4003
+ isRunning = true;
4004
+ try {
4005
+ const now = Math.floor(Date.now() / 1e3);
4006
+ const queryThreshold = now - stallTimeoutS;
4007
+ const stalledExecutions = db.prepare(`SELECT id, workflow_name, input, started_at FROM workflow_executions
4008
+ WHERE status = 'running' AND started_at < ?`).all(queryThreshold);
4009
+ for (const execution of stalledExecutions) {
4010
+ let input = null;
4011
+ if (execution.input) {
4012
+ try {
4013
+ input = JSON.parse(execution.input);
4014
+ } catch {
4015
+ db.prepare(`UPDATE workflow_executions SET status = 'failed', error = 'Malformed workflow input payload', completed_at = ? WHERE id = ?`).run(now, execution.id);
4016
+ continue;
4017
+ }
4018
+ }
4019
+ db.prepare(`UPDATE workflow_steps SET status = 'pending', error = NULL
4020
+ WHERE execution_id = ? AND status = 'running'`).run(execution.id);
4021
+ try {
4022
+ const response = await fetch(workerUrl, {
4023
+ method: "POST",
4024
+ headers: {
4025
+ "Content-Type": "application/json",
4026
+ "X-Ploy-Workflow-Execution": "true",
4027
+ "X-Ploy-Workflow-Name": execution.workflow_name,
4028
+ "X-Ploy-Execution-Id": execution.id
4029
+ },
4030
+ body: JSON.stringify(input)
4031
+ });
4032
+ if (response.ok) {
4033
+ db.prepare(`UPDATE workflow_executions SET started_at = ? WHERE id = ?`).run(Math.floor(Date.now() / 1e3), execution.id);
4034
+ }
4035
+ } catch {
4036
+ }
4037
+ }
4038
+ } catch (err) {
4039
+ warn(`Failed to recover stalled workflows: ${err instanceof Error ? err.message : String(err)}`);
4040
+ } finally {
4041
+ isRunning = false;
4042
+ }
4043
+ })(), intervalMs);
4044
+ }
4045
+ var DEFAULT_STALL_TIMEOUT_S;
3821
4046
  var init_workflow_service = __esm({
3822
4047
  "../emulator/dist/services/workflow-service.js"() {
4048
+ init_logger();
4049
+ DEFAULT_STALL_TIMEOUT_S = 100 * 60;
3823
4050
  }
3824
4051
  });
3825
4052
  async function startMockServer(dbManager2, config, options = {}) {
3826
4053
  const app = new Hono();
3827
4054
  if (config.db) {
3828
- const dbHandler = createDbHandler(dbManager2.getD1Database);
4055
+ const dbHandler = createDbHandler(dbManager2.getDatabase);
3829
4056
  app.post("/db", dbHandler);
3830
4057
  }
3831
4058
  if (config.queue) {
@@ -3836,6 +4063,8 @@ async function startMockServer(dbManager2, config, options = {}) {
3836
4063
  app.post("/queue/ack", queueHandlers.ackHandler);
3837
4064
  app.post("/queue/retry", queueHandlers.retryHandler);
3838
4065
  }
4066
+ let stalledRecoveryInterval;
4067
+ let timeoutEnforcementInterval;
3839
4068
  if (config.workflow) {
3840
4069
  const workflowHandlers = createWorkflowHandlers(dbManager2.emulatorDb, options.workerUrl);
3841
4070
  app.post("/workflow/trigger", workflowHandlers.triggerHandler);
@@ -3846,6 +4075,28 @@ async function startMockServer(dbManager2, config, options = {}) {
3846
4075
  app.post("/workflow/step/fail", workflowHandlers.stepFailHandler);
3847
4076
  app.post("/workflow/complete", workflowHandlers.completeHandler);
3848
4077
  app.post("/workflow/fail", workflowHandlers.failHandler);
4078
+ const perWorkflow = {};
4079
+ const perWorkflowStep = {};
4080
+ for (const [, value] of Object.entries(config.workflow)) {
4081
+ const workflowName = getWorkflowName(value);
4082
+ const timeout = getWorkflowTimeout(value);
4083
+ const stepTimeout = getWorkflowStepTimeout(value);
4084
+ if (timeout !== void 0) {
4085
+ perWorkflow[workflowName] = timeout;
4086
+ }
4087
+ if (stepTimeout !== void 0) {
4088
+ perWorkflowStep[workflowName] = stepTimeout;
4089
+ }
4090
+ }
4091
+ timeoutEnforcementInterval = startWorkflowTimeoutEnforcement(dbManager2.emulatorDb, 5e3, {
4092
+ perWorkflow,
4093
+ perWorkflowStep,
4094
+ defaultTimeoutS: DEFAULT_WORKFLOW_TIMEOUT_S,
4095
+ defaultStepTimeoutS: DEFAULT_STEP_TIMEOUT_S
4096
+ });
4097
+ if (options.workerUrl) {
4098
+ stalledRecoveryInterval = startStalledWorkflowRecovery(dbManager2.emulatorDb, options.workerUrl, 5e3, DEFAULT_STALL_TIMEOUT_S);
4099
+ }
3849
4100
  }
3850
4101
  if (config.cache) {
3851
4102
  const cacheHandlers = createCacheHandlers(dbManager2.emulatorDb);
@@ -3893,6 +4144,12 @@ async function startMockServer(dbManager2, config, options = {}) {
3893
4144
  resolve({
3894
4145
  port: info.port,
3895
4146
  close: () => new Promise((res) => {
4147
+ if (stalledRecoveryInterval) {
4148
+ clearInterval(stalledRecoveryInterval);
4149
+ }
4150
+ if (timeoutEnforcementInterval) {
4151
+ clearInterval(timeoutEnforcementInterval);
4152
+ }
3896
4153
  server.close(() => {
3897
4154
  res();
3898
4155
  });
@@ -3904,6 +4161,7 @@ async function startMockServer(dbManager2, config, options = {}) {
3904
4161
  var DEFAULT_MOCK_SERVER_PORT;
3905
4162
  var init_mock_server = __esm({
3906
4163
  "../emulator/dist/services/mock-server.js"() {
4164
+ init_ploy_config2();
3907
4165
  init_paths();
3908
4166
  init_auth_service();
3909
4167
  init_cache_service();
@@ -3980,29 +4238,29 @@ var init_sqlite_db = __esm({
3980
4238
  });
3981
4239
  function initializeDatabases(projectDir) {
3982
4240
  const dataDir = ensureDataDir(projectDir);
3983
- const d1Databases = /* @__PURE__ */ new Map();
4241
+ const databases = /* @__PURE__ */ new Map();
3984
4242
  const emulatorDb = openDatabase(join(dataDir, "emulator.db"));
3985
4243
  emulatorDb.pragma("journal_mode = WAL");
3986
4244
  emulatorDb.exec(EMULATOR_SCHEMA);
3987
- function getD1Database(bindingName) {
3988
- let db = d1Databases.get(bindingName);
4245
+ function getDatabase(bindingName) {
4246
+ let db = databases.get(bindingName);
3989
4247
  if (!db) {
3990
4248
  db = openDatabase(join(dataDir, "db", `${bindingName}.db`));
3991
4249
  db.pragma("journal_mode = WAL");
3992
- d1Databases.set(bindingName, db);
4250
+ databases.set(bindingName, db);
3993
4251
  }
3994
4252
  return db;
3995
4253
  }
3996
4254
  function close() {
3997
4255
  emulatorDb.close();
3998
- for (const db of d1Databases.values()) {
4256
+ for (const db of databases.values()) {
3999
4257
  db.close();
4000
4258
  }
4001
- d1Databases.clear();
4259
+ databases.clear();
4002
4260
  }
4003
4261
  return {
4004
4262
  emulatorDb,
4005
- getD1Database,
4263
+ getDatabase,
4006
4264
  close
4007
4265
  };
4008
4266
  }
@@ -4477,7 +4735,7 @@ var init_emulator = __esm({
4477
4735
  });
4478
4736
 
4479
4737
  // ../emulator/dist/nextjs-dev.js
4480
- function createDevD1(databaseId, apiUrl) {
4738
+ function createDevDB(databaseId, apiUrl) {
4481
4739
  return {
4482
4740
  async dump() {
4483
4741
  const response = await fetch(`${apiUrl}/db`, {
@@ -4642,7 +4900,7 @@ async function initPloyForDev(config) {
4642
4900
  const env2 = {};
4643
4901
  if (hasDbBindings2 && ployConfig2.db) {
4644
4902
  for (const [bindingName, databaseId] of Object.entries(ployConfig2.db)) {
4645
- env2[bindingName] = createDevD1(databaseId, cliMockServerUrl);
4903
+ env2[bindingName] = createDevDB(databaseId, cliMockServerUrl);
4646
4904
  }
4647
4905
  }
4648
4906
  if (hasAuthConfig2) {
@@ -4694,7 +4952,7 @@ async function initPloyForDev(config) {
4694
4952
  const env = {};
4695
4953
  if (hasDbBindings && ployConfig.db) {
4696
4954
  for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
4697
- env[bindingName] = createDevD1(databaseId, apiUrl);
4955
+ env[bindingName] = createDevDB(databaseId, apiUrl);
4698
4956
  }
4699
4957
  }
4700
4958
  if (hasAuthConfig) {
@@ -4827,7 +5085,7 @@ var init_dist2 = __esm({
4827
5085
  init_emulator();
4828
5086
  init_nextjs_dev();
4829
5087
  init_dev_dashboard();
4830
- init_db_runtime();
5088
+ init_db_runtime2();
4831
5089
  }
4832
5090
  });
4833
5091
  var CONFIG_DIR = join(homedir(), ".ploy");
@@ -6316,7 +6574,7 @@ ${varProps}
6316
6574
  }
6317
6575
  if (config.db) {
6318
6576
  for (const bindingName of Object.keys(config.db)) {
6319
- properties.push(` ${bindingName}: D1Database;`);
6577
+ properties.push(` ${bindingName}: Database;`);
6320
6578
  }
6321
6579
  }
6322
6580
  if (config.queue) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetploy/cli",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",