@meetploy/cli 1.16.0 → 1.17.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
@@ -7,9 +7,9 @@ import 'esbuild';
7
7
  import 'chokidar';
8
8
  import { promisify } from 'util';
9
9
  import { parse } from 'yaml';
10
+ import { randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, randomBytes } from 'crypto';
10
11
  import { serve } from '@hono/node-server';
11
12
  import { Hono } from 'hono';
12
- import { randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, randomBytes } from 'crypto';
13
13
  import 'os';
14
14
  import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
15
15
  import Database from 'better-sqlite3';
@@ -380,6 +380,7 @@ function createDashboardRoutes(app, dbManager2, config) {
380
380
  state: config.state,
381
381
  fs: config.fs,
382
382
  workflow: config.workflow,
383
+ cron: config.cron,
383
384
  auth: config.auth
384
385
  });
385
386
  });
@@ -904,6 +905,36 @@ function createDashboardRoutes(app, dbManager2, config) {
904
905
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
905
906
  }
906
907
  });
908
+ app.get("/api/cron/:binding/executions", (c) => {
909
+ const binding = c.req.param("binding");
910
+ const cronExpression = config.cron?.[binding];
911
+ const limit = parseInt(c.req.query("limit") ?? "20", 10);
912
+ if (!cronExpression) {
913
+ return c.json({ error: "Cron binding not found" }, 404);
914
+ }
915
+ try {
916
+ const db = dbManager2.emulatorDb;
917
+ const executions = db.prepare(`SELECT id, cron_name, cron_expression, status, error, duration_ms, triggered_at, completed_at, created_at
918
+ FROM cron_executions
919
+ WHERE cron_name = ?
920
+ ORDER BY triggered_at DESC
921
+ LIMIT ?`).all(binding, limit);
922
+ return c.json({
923
+ cronExpression,
924
+ executions: executions.map((e) => ({
925
+ id: e.id,
926
+ status: e.status.toUpperCase(),
927
+ error: e.error,
928
+ durationMs: e.duration_ms,
929
+ triggeredAt: new Date(e.triggered_at * 1e3).toISOString(),
930
+ completedAt: e.completed_at ? new Date(e.completed_at * 1e3).toISOString() : null,
931
+ createdAt: new Date(e.created_at * 1e3).toISOString()
932
+ }))
933
+ });
934
+ } catch (err) {
935
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
936
+ }
937
+ });
907
938
  if (hasDashboard) {
908
939
  app.get("/assets/*", (c) => {
909
940
  const path = c.req.path;
@@ -1654,6 +1685,22 @@ CREATE TABLE IF NOT EXISTS fs_entries (
1654
1685
  created_at INTEGER DEFAULT (strftime('%s', 'now')),
1655
1686
  PRIMARY KEY (fs_name, key)
1656
1687
  );
1688
+
1689
+ -- Cron executions table
1690
+ CREATE TABLE IF NOT EXISTS cron_executions (
1691
+ id TEXT PRIMARY KEY,
1692
+ cron_name TEXT NOT NULL,
1693
+ cron_expression TEXT NOT NULL,
1694
+ status TEXT DEFAULT 'running',
1695
+ error TEXT,
1696
+ duration_ms INTEGER,
1697
+ triggered_at INTEGER NOT NULL,
1698
+ completed_at INTEGER,
1699
+ created_at INTEGER DEFAULT (strftime('%s', 'now'))
1700
+ );
1701
+
1702
+ CREATE INDEX IF NOT EXISTS idx_cron_executions_name
1703
+ ON cron_executions(cron_name, triggered_at);
1657
1704
  `;
1658
1705
  function initializeDatabases(projectDir) {
1659
1706
  const dataDir = ensureDataDir(projectDir);
package/dist/index.js CHANGED
@@ -147,6 +147,22 @@ function validateBindings(bindings, bindingType, configFile) {
147
147
  }
148
148
  }
149
149
  }
150
+ function validateCronBindings(bindings, configFile) {
151
+ if (bindings === void 0) {
152
+ return;
153
+ }
154
+ for (const [bindingName, cronExpression] of Object.entries(bindings)) {
155
+ if (!BINDING_NAME_REGEX.test(bindingName)) {
156
+ throw new Error(`Invalid cron binding name '${bindingName}' in ${configFile}. Binding names must be uppercase with underscores (e.g., HOURLY_CLEANUP, DAILY_REPORT)`);
157
+ }
158
+ if (typeof cronExpression !== "string") {
159
+ throw new Error(`Cron binding '${bindingName}' in ${configFile} must have a string value (cron expression)`);
160
+ }
161
+ if (!CRON_EXPRESSION_REGEX.test(cronExpression.trim())) {
162
+ throw new Error(`Invalid cron expression '${cronExpression}' for binding '${bindingName}' in ${configFile}. Must be a valid cron expression with 5 fields (e.g., "0 * * * *", "*/5 * * * *")`);
163
+ }
164
+ }
165
+ }
150
166
  function validateRelativePath(path, fieldName, configFile) {
151
167
  if (path === void 0) {
152
168
  return void 0;
@@ -202,6 +218,7 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
202
218
  validateBindings(config.state, "state", configFile);
203
219
  validateBindings(config.fs, "fs", configFile);
204
220
  validateBindings(config.workflow, "workflow", configFile);
221
+ validateCronBindings(config.cron, configFile);
205
222
  if (config.ai !== void 0 && typeof config.ai !== "boolean") {
206
223
  throw new Error(`'ai' in ${configFile} must be a boolean`);
207
224
  }
@@ -277,7 +294,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
277
294
  return validatePloyConfig(config, configFile, validationOptions);
278
295
  }
279
296
  function hasBindings(config) {
280
- return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.ai ?? config.auth);
297
+ return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.cron ?? config.ai ?? config.auth);
281
298
  }
282
299
  function parseDotEnv(content) {
283
300
  const result = {};
@@ -339,7 +356,7 @@ function getWorkerEntryPoint(projectDir, config) {
339
356
  }
340
357
  throw new Error("Could not find worker entry point. Specify 'main' in ploy.yaml");
341
358
  }
342
- var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX;
359
+ var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
343
360
  var init_ploy_config = __esm({
344
361
  "../tools/dist/ploy-config.js"() {
345
362
  readFileAsync = promisify(readFile$1);
@@ -347,6 +364,7 @@ var init_ploy_config = __esm({
347
364
  DEFAULT_COMPATIBILITY_DATE = "2025-04-02";
348
365
  BINDING_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
349
366
  RESOURCE_NAME_REGEX = /^[a-z][a-z0-9_]*$/;
367
+ CRON_EXPRESSION_REGEX = /^(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)$/;
350
368
  }
351
369
  });
352
370
 
@@ -1280,6 +1298,37 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
1280
1298
  }
1281
1299
  }
1282
1300
  }` : "";
1301
+ const cronHandlerCode = config.cron ? `
1302
+ // Handle cron trigger delivery
1303
+ if (request.headers.get("X-Ploy-Cron-Trigger") === "true") {
1304
+ const cronExpression = request.headers.get("X-Ploy-Cron-Expression");
1305
+ const scheduledTime = parseInt(request.headers.get("X-Ploy-Cron-Scheduled-Time") || String(Date.now()), 10);
1306
+
1307
+ if (cronExpression && userWorker.scheduled) {
1308
+ let noRetryFlag = false;
1309
+ const event = {
1310
+ cron: cronExpression,
1311
+ scheduledTime,
1312
+ noRetry() { noRetryFlag = true; }
1313
+ };
1314
+ try {
1315
+ await userWorker.scheduled(event, injectedEnv, ctx);
1316
+ return new Response(JSON.stringify({ success: true, noRetry: noRetryFlag }), {
1317
+ headers: { "Content-Type": "application/json" }
1318
+ });
1319
+ } catch (error) {
1320
+ return new Response(JSON.stringify({ success: false, error: String(error) }), {
1321
+ status: 500,
1322
+ headers: { "Content-Type": "application/json" }
1323
+ });
1324
+ }
1325
+ }
1326
+
1327
+ return new Response(JSON.stringify({ success: false, error: "No scheduled handler defined" }), {
1328
+ status: 404,
1329
+ headers: { "Content-Type": "application/json" }
1330
+ });
1331
+ }` : "";
1283
1332
  const queueHandlerCode = config.queue ? `
1284
1333
  // Handle queue message delivery
1285
1334
  if (request.headers.get("X-Ploy-Queue-Delivery") === "true") {
@@ -1323,6 +1372,7 @@ ${bindings.join("\n")}
1323
1372
  export default {
1324
1373
  async fetch(request, env, ctx) {
1325
1374
  const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
1375
+ ${cronHandlerCode}
1326
1376
  ${workflowHandlerCode}
1327
1377
  ${queueHandlerCode}
1328
1378
 
@@ -1693,6 +1743,177 @@ var init_workerd_config = __esm({
1693
1743
  "../emulator/dist/config/workerd-config.js"() {
1694
1744
  }
1695
1745
  });
1746
+ function parseCronField(field, min, max) {
1747
+ const values = /* @__PURE__ */ new Set();
1748
+ for (const part of field.split(",")) {
1749
+ const trimmed = part.trim();
1750
+ if (trimmed === "*") {
1751
+ for (let i = min; i <= max; i++) {
1752
+ values.add(i);
1753
+ }
1754
+ continue;
1755
+ }
1756
+ const stepMatch = trimmed.match(/^(.+)\/(\d+)$/);
1757
+ if (stepMatch) {
1758
+ const step = parseInt(stepMatch[2], 10);
1759
+ const base = stepMatch[1];
1760
+ let start = min;
1761
+ let end = max;
1762
+ if (base !== "*") {
1763
+ const rangeMatch2 = base.match(/^(\d+)-(\d+)$/);
1764
+ if (rangeMatch2) {
1765
+ start = parseInt(rangeMatch2[1], 10);
1766
+ end = parseInt(rangeMatch2[2], 10);
1767
+ } else {
1768
+ start = parseInt(base, 10);
1769
+ }
1770
+ }
1771
+ if (start < min || start > max || end < min || end > max) {
1772
+ throw new Error(`Cron field range ${start}-${end} is out of bounds (${min}-${max})`);
1773
+ }
1774
+ for (let i = start; i <= end; i += step) {
1775
+ values.add(i);
1776
+ }
1777
+ continue;
1778
+ }
1779
+ const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
1780
+ if (rangeMatch) {
1781
+ const start = parseInt(rangeMatch[1], 10);
1782
+ const end = parseInt(rangeMatch[2], 10);
1783
+ if (start < min || start > max || end < min || end > max) {
1784
+ throw new Error(`Cron field range ${start}-${end} is out of bounds (${min}-${max})`);
1785
+ }
1786
+ for (let i = start; i <= end; i++) {
1787
+ values.add(i);
1788
+ }
1789
+ continue;
1790
+ }
1791
+ const num = parseInt(trimmed, 10);
1792
+ if (isNaN(num)) {
1793
+ throw new Error(`Invalid cron field value: '${trimmed}' (expected number, range, step, or wildcard)`);
1794
+ }
1795
+ if (num < min || num > max) {
1796
+ throw new Error(`Cron field value ${num} is out of range (${min}-${max})`);
1797
+ }
1798
+ values.add(num);
1799
+ }
1800
+ return { values };
1801
+ }
1802
+ function parseCronExpression(expression) {
1803
+ const parts = expression.trim().split(/\s+/);
1804
+ if (parts.length !== 5) {
1805
+ throw new Error(`Invalid cron expression: ${expression} (expected 5 fields)`);
1806
+ }
1807
+ return [
1808
+ parseCronField(parts[0], 0, 59),
1809
+ parseCronField(parts[1], 0, 23),
1810
+ parseCronField(parts[2], 1, 31),
1811
+ parseCronField(parts[3], 1, 12),
1812
+ parseCronField(parts[4], 0, 6)
1813
+ ];
1814
+ }
1815
+ function cronMatchesDate(fields, date) {
1816
+ const minute = date.getMinutes();
1817
+ const hour = date.getHours();
1818
+ const dayOfMonth = date.getDate();
1819
+ const month = date.getMonth() + 1;
1820
+ const dayOfWeek = date.getDay();
1821
+ return fields[0].values.has(minute) && fields[1].values.has(hour) && fields[2].values.has(dayOfMonth) && fields[3].values.has(month) && fields[4].values.has(dayOfWeek);
1822
+ }
1823
+ function createCronScheduler(db, cronBindings, workerUrl) {
1824
+ let interval = null;
1825
+ const parsedCrons = /* @__PURE__ */ new Map();
1826
+ let lastCheckedMinute = -1;
1827
+ for (const [name, expression] of Object.entries(cronBindings)) {
1828
+ try {
1829
+ const fields = parseCronExpression(expression);
1830
+ parsedCrons.set(name, { expression, fields });
1831
+ } catch (err) {
1832
+ error(`Failed to parse cron expression for '${name}': ${err instanceof Error ? err.message : String(err)}`);
1833
+ }
1834
+ }
1835
+ async function checkCrons() {
1836
+ const now = /* @__PURE__ */ new Date();
1837
+ const currentMinute = Math.floor(now.getTime() / 6e4);
1838
+ if (currentMinute === lastCheckedMinute) {
1839
+ return;
1840
+ }
1841
+ lastCheckedMinute = currentMinute;
1842
+ for (const [name, cron] of parsedCrons) {
1843
+ if (cronMatchesDate(cron.fields, now)) {
1844
+ await triggerScheduledHandler(db, name, cron.expression, now, workerUrl);
1845
+ }
1846
+ }
1847
+ }
1848
+ return {
1849
+ start() {
1850
+ if (interval) {
1851
+ return;
1852
+ }
1853
+ log(`Cron scheduler started with ${parsedCrons.size} trigger(s)`);
1854
+ for (const [name, cron] of parsedCrons) {
1855
+ log(` ${name}: ${cron.expression}`);
1856
+ }
1857
+ interval = setInterval(() => {
1858
+ checkCrons().catch((err) => {
1859
+ error(`Cron check error: ${err instanceof Error ? err.message : String(err)}`);
1860
+ });
1861
+ }, 15e3);
1862
+ checkCrons().catch(() => {
1863
+ });
1864
+ },
1865
+ stop() {
1866
+ if (interval) {
1867
+ clearInterval(interval);
1868
+ interval = null;
1869
+ }
1870
+ }
1871
+ };
1872
+ }
1873
+ async function triggerScheduledHandler(db, cronName, cronExpression, triggerTime, workerUrl) {
1874
+ const executionId = randomUUID();
1875
+ const now = Math.floor(Date.now() / 1e3);
1876
+ db.prepare(`INSERT INTO cron_executions (id, cron_name, cron_expression, status, triggered_at)
1877
+ VALUES (?, ?, ?, 'running', ?)`).run(executionId, cronName, cronExpression, now);
1878
+ log(`[cron] Triggering '${cronName}' (${cronExpression}) execution=${executionId}`);
1879
+ try {
1880
+ const response = await fetch(workerUrl, {
1881
+ method: "POST",
1882
+ headers: {
1883
+ "Content-Type": "application/json",
1884
+ "X-Ploy-Cron-Trigger": "true",
1885
+ "X-Ploy-Cron-Name": cronName,
1886
+ "X-Ploy-Cron-Expression": cronExpression,
1887
+ "X-Ploy-Cron-Execution-Id": executionId,
1888
+ "X-Ploy-Cron-Scheduled-Time": String(triggerTime.getTime())
1889
+ },
1890
+ body: JSON.stringify({
1891
+ cron: cronExpression,
1892
+ scheduledTime: triggerTime.getTime()
1893
+ })
1894
+ });
1895
+ const completedAt = Math.floor(Date.now() / 1e3);
1896
+ const durationMs = Date.now() - triggerTime.getTime();
1897
+ if (response.ok) {
1898
+ db.prepare(`UPDATE cron_executions SET status = 'completed', completed_at = ?, duration_ms = ? WHERE id = ?`).run(completedAt, durationMs, executionId);
1899
+ log(`[cron] '${cronName}' completed successfully (${durationMs}ms)`);
1900
+ } else {
1901
+ const errorText = await response.text();
1902
+ db.prepare(`UPDATE cron_executions SET status = 'failed', completed_at = ?, duration_ms = ?, error = ? WHERE id = ?`).run(completedAt, durationMs, errorText, executionId);
1903
+ error(`[cron] '${cronName}' failed: ${errorText}`);
1904
+ }
1905
+ } catch (err) {
1906
+ const completedAt = Math.floor(Date.now() / 1e3);
1907
+ const errorMessage = err instanceof Error ? err.message : String(err);
1908
+ db.prepare(`UPDATE cron_executions SET status = 'failed', completed_at = ?, error = ? WHERE id = ?`).run(completedAt, errorMessage, executionId);
1909
+ error(`[cron] '${cronName}' error: ${errorMessage}`);
1910
+ }
1911
+ }
1912
+ var init_cron_service = __esm({
1913
+ "../emulator/dist/services/cron-service.js"() {
1914
+ init_logger();
1915
+ }
1916
+ });
1696
1917
  function getProjectHash(projectDir) {
1697
1918
  return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
1698
1919
  }
@@ -2035,6 +2256,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2035
2256
  state: config.state,
2036
2257
  fs: config.fs,
2037
2258
  workflow: config.workflow,
2259
+ cron: config.cron,
2038
2260
  auth: config.auth
2039
2261
  });
2040
2262
  });
@@ -2559,6 +2781,36 @@ function createDashboardRoutes(app, dbManager2, config) {
2559
2781
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2560
2782
  }
2561
2783
  });
2784
+ app.get("/api/cron/:binding/executions", (c) => {
2785
+ const binding = c.req.param("binding");
2786
+ const cronExpression = config.cron?.[binding];
2787
+ const limit = parseInt(c.req.query("limit") ?? "20", 10);
2788
+ if (!cronExpression) {
2789
+ return c.json({ error: "Cron binding not found" }, 404);
2790
+ }
2791
+ try {
2792
+ const db = dbManager2.emulatorDb;
2793
+ const executions = db.prepare(`SELECT id, cron_name, cron_expression, status, error, duration_ms, triggered_at, completed_at, created_at
2794
+ FROM cron_executions
2795
+ WHERE cron_name = ?
2796
+ ORDER BY triggered_at DESC
2797
+ LIMIT ?`).all(binding, limit);
2798
+ return c.json({
2799
+ cronExpression,
2800
+ executions: executions.map((e) => ({
2801
+ id: e.id,
2802
+ status: e.status.toUpperCase(),
2803
+ error: e.error,
2804
+ durationMs: e.duration_ms,
2805
+ triggeredAt: new Date(e.triggered_at * 1e3).toISOString(),
2806
+ completedAt: e.completed_at ? new Date(e.completed_at * 1e3).toISOString() : null,
2807
+ createdAt: new Date(e.created_at * 1e3).toISOString()
2808
+ }))
2809
+ });
2810
+ } catch (err) {
2811
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2812
+ }
2813
+ });
2562
2814
  if (hasDashboard) {
2563
2815
  app.get("/assets/*", (c) => {
2564
2816
  const path = c.req.path;
@@ -3463,6 +3715,22 @@ CREATE TABLE IF NOT EXISTS fs_entries (
3463
3715
  created_at INTEGER DEFAULT (strftime('%s', 'now')),
3464
3716
  PRIMARY KEY (fs_name, key)
3465
3717
  );
3718
+
3719
+ -- Cron executions table
3720
+ CREATE TABLE IF NOT EXISTS cron_executions (
3721
+ id TEXT PRIMARY KEY,
3722
+ cron_name TEXT NOT NULL,
3723
+ cron_expression TEXT NOT NULL,
3724
+ status TEXT DEFAULT 'running',
3725
+ error TEXT,
3726
+ duration_ms INTEGER,
3727
+ triggered_at INTEGER NOT NULL,
3728
+ completed_at INTEGER,
3729
+ created_at INTEGER DEFAULT (strftime('%s', 'now'))
3730
+ );
3731
+
3732
+ CREATE INDEX IF NOT EXISTS idx_cron_executions_name
3733
+ ON cron_executions(cron_name, triggered_at);
3466
3734
  `;
3467
3735
  }
3468
3736
  });
@@ -3479,6 +3747,7 @@ var init_emulator = __esm({
3479
3747
  init_env();
3480
3748
  init_ploy_config2();
3481
3749
  init_workerd_config();
3750
+ init_cron_service();
3482
3751
  init_mock_server();
3483
3752
  init_queue_service();
3484
3753
  init_logger();
@@ -3494,6 +3763,7 @@ var init_emulator = __esm({
3494
3763
  workerdProcess = null;
3495
3764
  fileWatcher = null;
3496
3765
  queueProcessor = null;
3766
+ cronScheduler = null;
3497
3767
  resolvedEnvVars = {};
3498
3768
  constructor(options = {}) {
3499
3769
  const port = options.port ?? 8787;
@@ -3561,6 +3831,12 @@ var init_emulator = __esm({
3561
3831
  this.queueProcessor.start();
3562
3832
  debug("Queue processor started", this.options.verbose);
3563
3833
  }
3834
+ if (this.config.cron) {
3835
+ const workerUrl2 = `http://${this.options.host}:${String(this.options.port)}`;
3836
+ this.cronScheduler = createCronScheduler(this.dbManager.emulatorDb, this.config.cron, workerUrl2);
3837
+ this.cronScheduler.start();
3838
+ debug("Cron scheduler started", this.options.verbose);
3839
+ }
3564
3840
  success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
3565
3841
  log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
3566
3842
  if (Object.keys(this.resolvedEnvVars).length > 0) {
@@ -3581,6 +3857,11 @@ var init_emulator = __esm({
3581
3857
  if (this.config.workflow) {
3582
3858
  log(` Workflow bindings: ${Object.keys(this.config.workflow).join(", ")}`);
3583
3859
  }
3860
+ if (this.config.cron) {
3861
+ for (const [name, expr] of Object.entries(this.config.cron)) {
3862
+ log(` Cron: ${name} = ${expr}`);
3863
+ }
3864
+ }
3584
3865
  this.setupSignalHandlers();
3585
3866
  } catch (err) {
3586
3867
  error(`Failed to start emulator: ${err instanceof Error ? err.message : String(err)}`);
@@ -3705,6 +3986,10 @@ var init_emulator = __esm({
3705
3986
  process.on("SIGTERM", handler);
3706
3987
  }
3707
3988
  async stop() {
3989
+ if (this.cronScheduler) {
3990
+ this.cronScheduler.stop();
3991
+ this.cronScheduler = null;
3992
+ }
3708
3993
  if (this.queueProcessor) {
3709
3994
  this.queueProcessor.stop();
3710
3995
  this.queueProcessor = null;
@@ -5641,7 +5926,7 @@ async function typesCommand(options = {}) {
5641
5926
  console.error("Error: ploy.yaml not found in current directory");
5642
5927
  process.exit(1);
5643
5928
  }
5644
- const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow;
5929
+ const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow || config.cron;
5645
5930
  if (!hasBindings2) {
5646
5931
  console.log("No bindings found in ploy.yaml. Generating empty Env.");
5647
5932
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetploy/cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",