@meetploy/cli 1.15.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/dashboard-dist/assets/{main-qA3kxECS.css → main-B-euJxpr.css} +1 -1
- package/dist/dashboard-dist/assets/main-duAiLjPq.js +339 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/dev.js +238 -15
- package/dist/index.js +625 -32
- package/package.json +1 -1
- package/dist/dashboard-dist/assets/main-UY1Z1kG0.js +0 -334
package/dist/index.js
CHANGED
|
@@ -8,11 +8,11 @@ import { parse } from 'yaml';
|
|
|
8
8
|
import { build } from 'esbuild';
|
|
9
9
|
import { watch } from 'chokidar';
|
|
10
10
|
import { randomBytes, randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, createHash } from 'crypto';
|
|
11
|
+
import { homedir, tmpdir } from 'os';
|
|
11
12
|
import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
|
|
12
13
|
import { URL, fileURLToPath } from 'url';
|
|
13
14
|
import { serve } from '@hono/node-server';
|
|
14
15
|
import { Hono } from 'hono';
|
|
15
|
-
import { homedir, tmpdir } from 'os';
|
|
16
16
|
import Database from 'better-sqlite3';
|
|
17
17
|
import { spawn, exec } from 'child_process';
|
|
18
18
|
import createClient from 'openapi-fetch';
|
|
@@ -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;
|
|
@@ -200,7 +216,9 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
|
|
|
200
216
|
validateBindings(config.queue, "queue", configFile);
|
|
201
217
|
validateBindings(config.cache, "cache", configFile);
|
|
202
218
|
validateBindings(config.state, "state", configFile);
|
|
219
|
+
validateBindings(config.fs, "fs", configFile);
|
|
203
220
|
validateBindings(config.workflow, "workflow", configFile);
|
|
221
|
+
validateCronBindings(config.cron, configFile);
|
|
204
222
|
if (config.ai !== void 0 && typeof config.ai !== "boolean") {
|
|
205
223
|
throw new Error(`'ai' in ${configFile} must be a boolean`);
|
|
206
224
|
}
|
|
@@ -276,7 +294,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
|
|
|
276
294
|
return validatePloyConfig(config, configFile, validationOptions);
|
|
277
295
|
}
|
|
278
296
|
function hasBindings(config) {
|
|
279
|
-
return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? 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);
|
|
280
298
|
}
|
|
281
299
|
function parseDotEnv(content) {
|
|
282
300
|
const result = {};
|
|
@@ -338,7 +356,7 @@ function getWorkerEntryPoint(projectDir, config) {
|
|
|
338
356
|
}
|
|
339
357
|
throw new Error("Could not find worker entry point. Specify 'main' in ploy.yaml");
|
|
340
358
|
}
|
|
341
|
-
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;
|
|
342
360
|
var init_ploy_config = __esm({
|
|
343
361
|
"../tools/dist/ploy-config.js"() {
|
|
344
362
|
readFileAsync = promisify(readFile$1);
|
|
@@ -346,6 +364,7 @@ var init_ploy_config = __esm({
|
|
|
346
364
|
DEFAULT_COMPATIBILITY_DATE = "2025-04-02";
|
|
347
365
|
BINDING_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
348
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,\-/]+)$/;
|
|
349
368
|
}
|
|
350
369
|
});
|
|
351
370
|
|
|
@@ -757,6 +776,124 @@ var init_db_runtime = __esm({
|
|
|
757
776
|
}
|
|
758
777
|
});
|
|
759
778
|
|
|
779
|
+
// ../emulator/dist/runtime/fs-runtime.js
|
|
780
|
+
var FS_RUNTIME_CODE;
|
|
781
|
+
var init_fs_runtime = __esm({
|
|
782
|
+
"../emulator/dist/runtime/fs-runtime.js"() {
|
|
783
|
+
FS_RUNTIME_CODE = `
|
|
784
|
+
interface FileStoragePutOptions {
|
|
785
|
+
contentType?: string;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
interface FileStorageListOptions {
|
|
789
|
+
prefix?: string;
|
|
790
|
+
limit?: number;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
interface FileStorageObject {
|
|
794
|
+
body: string;
|
|
795
|
+
contentType: string;
|
|
796
|
+
size: number;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
interface FileStorageKey {
|
|
800
|
+
key: string;
|
|
801
|
+
size: number;
|
|
802
|
+
contentType: string;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
interface FileStorageListResult {
|
|
806
|
+
keys: FileStorageKey[];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
interface FileStorageBinding {
|
|
810
|
+
put: (key: string, value: string, options?: FileStoragePutOptions) => Promise<void>;
|
|
811
|
+
get: (key: string) => Promise<FileStorageObject | null>;
|
|
812
|
+
delete: (key: string) => Promise<void>;
|
|
813
|
+
list: (options?: FileStorageListOptions) => Promise<FileStorageListResult>;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function initializeFileStorage(fsName: string, serviceUrl: string): FileStorageBinding {
|
|
817
|
+
return {
|
|
818
|
+
async put(key: string, value: string, options?: FileStoragePutOptions): Promise<void> {
|
|
819
|
+
const response = await fetch(serviceUrl + "/fs/put", {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
fsName,
|
|
824
|
+
key,
|
|
825
|
+
value,
|
|
826
|
+
contentType: options?.contentType || "application/octet-stream",
|
|
827
|
+
}),
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
if (!response.ok) {
|
|
831
|
+
const errorText = await response.text();
|
|
832
|
+
throw new Error("File storage put failed: " + errorText);
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
async get(key: string): Promise<FileStorageObject | null> {
|
|
837
|
+
const response = await fetch(serviceUrl + "/fs/get", {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: { "Content-Type": "application/json" },
|
|
840
|
+
body: JSON.stringify({ fsName, key }),
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
if (!response.ok) {
|
|
844
|
+
const errorText = await response.text();
|
|
845
|
+
throw new Error("File storage get failed: " + errorText);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const result = await response.json();
|
|
849
|
+
if (result.found === false) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
body: result.body,
|
|
854
|
+
contentType: result.contentType,
|
|
855
|
+
size: result.size,
|
|
856
|
+
};
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
async delete(key: string): Promise<void> {
|
|
860
|
+
const response = await fetch(serviceUrl + "/fs/delete", {
|
|
861
|
+
method: "POST",
|
|
862
|
+
headers: { "Content-Type": "application/json" },
|
|
863
|
+
body: JSON.stringify({ fsName, key }),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
if (!response.ok) {
|
|
867
|
+
const errorText = await response.text();
|
|
868
|
+
throw new Error("File storage delete failed: " + errorText);
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
async list(options?: FileStorageListOptions): Promise<FileStorageListResult> {
|
|
873
|
+
const response = await fetch(serviceUrl + "/fs/list", {
|
|
874
|
+
method: "POST",
|
|
875
|
+
headers: { "Content-Type": "application/json" },
|
|
876
|
+
body: JSON.stringify({
|
|
877
|
+
fsName,
|
|
878
|
+
prefix: options?.prefix,
|
|
879
|
+
limit: options?.limit,
|
|
880
|
+
}),
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (!response.ok) {
|
|
884
|
+
const errorText = await response.text();
|
|
885
|
+
throw new Error("File storage list failed: " + errorText);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const result = await response.json();
|
|
889
|
+
return { keys: result.keys };
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
760
897
|
// ../emulator/dist/runtime/queue-runtime.js
|
|
761
898
|
var QUEUE_RUNTIME_CODE;
|
|
762
899
|
var init_queue_runtime = __esm({
|
|
@@ -1121,6 +1258,12 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1121
1258
|
bindings.push(` ${bindingName}: initializeState("${stateName}", "${mockServiceUrl}"),`);
|
|
1122
1259
|
}
|
|
1123
1260
|
}
|
|
1261
|
+
if (config.fs) {
|
|
1262
|
+
imports.push('import { initializeFileStorage } from "__ploy_fs_runtime__";');
|
|
1263
|
+
for (const [bindingName, fsName] of Object.entries(config.fs)) {
|
|
1264
|
+
bindings.push(` ${bindingName}: initializeFileStorage("${fsName}", "${mockServiceUrl}"),`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1124
1267
|
if (config.workflow) {
|
|
1125
1268
|
imports.push('import { initializeWorkflow, createStepContext, executeWorkflow } from "__ploy_workflow_runtime__";');
|
|
1126
1269
|
for (const [bindingName, workflowName] of Object.entries(config.workflow)) {
|
|
@@ -1155,6 +1298,37 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1155
1298
|
}
|
|
1156
1299
|
}
|
|
1157
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
|
+
}` : "";
|
|
1158
1332
|
const queueHandlerCode = config.queue ? `
|
|
1159
1333
|
// Handle queue message delivery
|
|
1160
1334
|
if (request.headers.get("X-Ploy-Queue-Delivery") === "true") {
|
|
@@ -1198,6 +1372,7 @@ ${bindings.join("\n")}
|
|
|
1198
1372
|
export default {
|
|
1199
1373
|
async fetch(request, env, ctx) {
|
|
1200
1374
|
const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
|
|
1375
|
+
${cronHandlerCode}
|
|
1201
1376
|
${workflowHandlerCode}
|
|
1202
1377
|
${queueHandlerCode}
|
|
1203
1378
|
|
|
@@ -1237,6 +1412,10 @@ function createRuntimePlugin(_config) {
|
|
|
1237
1412
|
path: "__ploy_state_runtime__",
|
|
1238
1413
|
namespace: "ploy-runtime"
|
|
1239
1414
|
}));
|
|
1415
|
+
build2.onResolve({ filter: /^__ploy_fs_runtime__$/ }, () => ({
|
|
1416
|
+
path: "__ploy_fs_runtime__",
|
|
1417
|
+
namespace: "ploy-runtime"
|
|
1418
|
+
}));
|
|
1240
1419
|
build2.onResolve({ filter: /^__ploy_workflow_runtime__$/ }, () => ({
|
|
1241
1420
|
path: "__ploy_workflow_runtime__",
|
|
1242
1421
|
namespace: "ploy-runtime"
|
|
@@ -1257,6 +1436,10 @@ function createRuntimePlugin(_config) {
|
|
|
1257
1436
|
contents: STATE_RUNTIME_CODE,
|
|
1258
1437
|
loader: "ts"
|
|
1259
1438
|
}));
|
|
1439
|
+
build2.onLoad({ filter: /^__ploy_fs_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1440
|
+
contents: FS_RUNTIME_CODE,
|
|
1441
|
+
loader: "ts"
|
|
1442
|
+
}));
|
|
1260
1443
|
build2.onLoad({ filter: /^__ploy_workflow_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1261
1444
|
contents: WORKFLOW_RUNTIME_CODE,
|
|
1262
1445
|
loader: "ts"
|
|
@@ -1299,6 +1482,7 @@ var init_bundler = __esm({
|
|
|
1299
1482
|
"../emulator/dist/bundler/bundler.js"() {
|
|
1300
1483
|
init_cache_runtime();
|
|
1301
1484
|
init_db_runtime();
|
|
1485
|
+
init_fs_runtime();
|
|
1302
1486
|
init_queue_runtime();
|
|
1303
1487
|
init_state_runtime();
|
|
1304
1488
|
init_workflow_runtime();
|
|
@@ -1559,6 +1743,205 @@ var init_workerd_config = __esm({
|
|
|
1559
1743
|
"../emulator/dist/config/workerd-config.js"() {
|
|
1560
1744
|
}
|
|
1561
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
|
+
});
|
|
1917
|
+
function getProjectHash(projectDir) {
|
|
1918
|
+
return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
|
|
1919
|
+
}
|
|
1920
|
+
function getTempDir(projectDir) {
|
|
1921
|
+
const hash = getProjectHash(projectDir);
|
|
1922
|
+
return join(tmpdir(), `ploy-emulator-${hash}`);
|
|
1923
|
+
}
|
|
1924
|
+
function getDataDir(projectDir) {
|
|
1925
|
+
return join(projectDir, ".ploy");
|
|
1926
|
+
}
|
|
1927
|
+
function ensureDir(dir) {
|
|
1928
|
+
mkdirSync(dir, { recursive: true });
|
|
1929
|
+
}
|
|
1930
|
+
function ensureTempDir(projectDir) {
|
|
1931
|
+
const tempDir = getTempDir(projectDir);
|
|
1932
|
+
ensureDir(tempDir);
|
|
1933
|
+
return tempDir;
|
|
1934
|
+
}
|
|
1935
|
+
function ensureDataDir(projectDir) {
|
|
1936
|
+
const dataDir = getDataDir(projectDir);
|
|
1937
|
+
ensureDir(dataDir);
|
|
1938
|
+
ensureDir(join(dataDir, "db"));
|
|
1939
|
+
return dataDir;
|
|
1940
|
+
}
|
|
1941
|
+
var init_paths = __esm({
|
|
1942
|
+
"../emulator/dist/utils/paths.js"() {
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1562
1945
|
function generateId() {
|
|
1563
1946
|
return randomBytes(16).toString("hex");
|
|
1564
1947
|
}
|
|
@@ -1871,7 +2254,9 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
1871
2254
|
queue: config.queue,
|
|
1872
2255
|
cache: config.cache,
|
|
1873
2256
|
state: config.state,
|
|
2257
|
+
fs: config.fs,
|
|
1874
2258
|
workflow: config.workflow,
|
|
2259
|
+
cron: config.cron,
|
|
1875
2260
|
auth: config.auth
|
|
1876
2261
|
});
|
|
1877
2262
|
});
|
|
@@ -2365,6 +2750,67 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2365
2750
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2366
2751
|
}
|
|
2367
2752
|
});
|
|
2753
|
+
app.get("/api/fs/:binding/entries", (c) => {
|
|
2754
|
+
const binding = c.req.param("binding");
|
|
2755
|
+
const fsName = config.fs?.[binding];
|
|
2756
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
2757
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
2758
|
+
if (!fsName) {
|
|
2759
|
+
return c.json({ error: "File storage binding not found" }, 404);
|
|
2760
|
+
}
|
|
2761
|
+
try {
|
|
2762
|
+
const db = dbManager2.emulatorDb;
|
|
2763
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM fs_entries WHERE fs_name = ?`).get(fsName).count;
|
|
2764
|
+
const entries = db.prepare(`SELECT key, size, content_type, created_at
|
|
2765
|
+
FROM fs_entries
|
|
2766
|
+
WHERE fs_name = ?
|
|
2767
|
+
ORDER BY key ASC
|
|
2768
|
+
LIMIT ? OFFSET ?`).all(fsName, limit, offset);
|
|
2769
|
+
return c.json({
|
|
2770
|
+
entries: entries.map((e) => ({
|
|
2771
|
+
key: e.key,
|
|
2772
|
+
size: e.size,
|
|
2773
|
+
contentType: e.content_type,
|
|
2774
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
2775
|
+
})),
|
|
2776
|
+
total,
|
|
2777
|
+
limit,
|
|
2778
|
+
offset
|
|
2779
|
+
});
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2782
|
+
}
|
|
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
|
+
});
|
|
2368
2814
|
if (hasDashboard) {
|
|
2369
2815
|
app.get("/assets/*", (c) => {
|
|
2370
2816
|
const path = c.req.path;
|
|
@@ -2515,6 +2961,125 @@ var init_db_service = __esm({
|
|
|
2515
2961
|
"../emulator/dist/services/db-service.js"() {
|
|
2516
2962
|
}
|
|
2517
2963
|
});
|
|
2964
|
+
function createFsHandlers(db, dataDir) {
|
|
2965
|
+
const fsBaseDir = join(dataDir, "fs");
|
|
2966
|
+
mkdirSync(fsBaseDir, { recursive: true });
|
|
2967
|
+
function getFsDir(fsName) {
|
|
2968
|
+
const dir = join(fsBaseDir, fsName);
|
|
2969
|
+
mkdirSync(dir, { recursive: true });
|
|
2970
|
+
return dir;
|
|
2971
|
+
}
|
|
2972
|
+
function encodedKeyPath(fsDir, key) {
|
|
2973
|
+
return join(fsDir, encodeURIComponent(key));
|
|
2974
|
+
}
|
|
2975
|
+
const putHandler = async (c) => {
|
|
2976
|
+
try {
|
|
2977
|
+
const body = await c.req.json();
|
|
2978
|
+
const { fsName, key, value, contentType } = body;
|
|
2979
|
+
if (!fsName || !key) {
|
|
2980
|
+
return c.json({ error: "Missing required fields: fsName, key, value" }, 400);
|
|
2981
|
+
}
|
|
2982
|
+
const fsDir = getFsDir(fsName);
|
|
2983
|
+
const filePath = encodedKeyPath(fsDir, key);
|
|
2984
|
+
writeFileSync(filePath, value, "utf-8");
|
|
2985
|
+
const size = Buffer.byteLength(value, "utf-8");
|
|
2986
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2987
|
+
db.prepare(`INSERT OR REPLACE INTO fs_entries (fs_name, key, content_type, size, created_at) VALUES (?, ?, ?, ?, ?)`).run(fsName, key, contentType ?? "application/octet-stream", size, now);
|
|
2988
|
+
return c.json({ success: true });
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
error(`[fs-service] put error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2991
|
+
return c.json({
|
|
2992
|
+
error: `put failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2993
|
+
}, 500);
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
const getHandler = async (c) => {
|
|
2997
|
+
try {
|
|
2998
|
+
const body = await c.req.json();
|
|
2999
|
+
const { fsName, key } = body;
|
|
3000
|
+
if (!fsName || !key) {
|
|
3001
|
+
return c.json({ error: "Missing required fields: fsName, key" }, 400);
|
|
3002
|
+
}
|
|
3003
|
+
const row = db.prepare(`SELECT content_type, size FROM fs_entries WHERE fs_name = ? AND key = ?`).get(fsName, key);
|
|
3004
|
+
if (!row) {
|
|
3005
|
+
return c.json({ found: false });
|
|
3006
|
+
}
|
|
3007
|
+
const fsDir = getFsDir(fsName);
|
|
3008
|
+
const filePath = encodedKeyPath(fsDir, key);
|
|
3009
|
+
if (!existsSync(filePath)) {
|
|
3010
|
+
db.prepare(`DELETE FROM fs_entries WHERE fs_name = ? AND key = ?`).run(fsName, key);
|
|
3011
|
+
return c.json({ found: false });
|
|
3012
|
+
}
|
|
3013
|
+
const fileContent = readFileSync(filePath, "utf-8");
|
|
3014
|
+
return c.json({
|
|
3015
|
+
found: true,
|
|
3016
|
+
body: fileContent,
|
|
3017
|
+
contentType: row.content_type,
|
|
3018
|
+
size: row.size
|
|
3019
|
+
});
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
error(`[fs-service] get error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3022
|
+
return c.json({
|
|
3023
|
+
error: `get failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3024
|
+
}, 500);
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
const deleteHandler = async (c) => {
|
|
3028
|
+
try {
|
|
3029
|
+
const body = await c.req.json();
|
|
3030
|
+
const { fsName, key } = body;
|
|
3031
|
+
if (!fsName || !key) {
|
|
3032
|
+
return c.json({ error: "Missing required fields: fsName, key" }, 400);
|
|
3033
|
+
}
|
|
3034
|
+
db.prepare(`DELETE FROM fs_entries WHERE fs_name = ? AND key = ?`).run(fsName, key);
|
|
3035
|
+
const fsDir = getFsDir(fsName);
|
|
3036
|
+
const filePath = encodedKeyPath(fsDir, key);
|
|
3037
|
+
if (existsSync(filePath)) {
|
|
3038
|
+
unlinkSync(filePath);
|
|
3039
|
+
}
|
|
3040
|
+
return c.json({ success: true });
|
|
3041
|
+
} catch (err) {
|
|
3042
|
+
error(`[fs-service] delete error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3043
|
+
return c.json({
|
|
3044
|
+
error: `delete failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3045
|
+
}, 500);
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
const listHandler = async (c) => {
|
|
3049
|
+
try {
|
|
3050
|
+
const body = await c.req.json();
|
|
3051
|
+
const { fsName, prefix, limit } = body;
|
|
3052
|
+
if (!fsName) {
|
|
3053
|
+
return c.json({ error: "Missing required field: fsName" }, 400);
|
|
3054
|
+
}
|
|
3055
|
+
const effectiveLimit = limit ?? 1e3;
|
|
3056
|
+
const keys = prefix ? db.prepare(`SELECT key, size, content_type FROM fs_entries WHERE fs_name = ? AND key LIKE ? ORDER BY key ASC LIMIT ?`).all(fsName, `${prefix}%`, effectiveLimit) : db.prepare(`SELECT key, size, content_type FROM fs_entries WHERE fs_name = ? ORDER BY key ASC LIMIT ?`).all(fsName, effectiveLimit);
|
|
3057
|
+
return c.json({
|
|
3058
|
+
keys: keys.map((k) => ({
|
|
3059
|
+
key: k.key,
|
|
3060
|
+
size: k.size,
|
|
3061
|
+
contentType: k.content_type
|
|
3062
|
+
}))
|
|
3063
|
+
});
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
error(`[fs-service] list error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3066
|
+
return c.json({
|
|
3067
|
+
error: `list failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3068
|
+
}, 500);
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
return {
|
|
3072
|
+
putHandler,
|
|
3073
|
+
getHandler,
|
|
3074
|
+
deleteHandler,
|
|
3075
|
+
listHandler
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
var init_fs_service = __esm({
|
|
3079
|
+
"../emulator/dist/services/fs-service.js"() {
|
|
3080
|
+
init_logger();
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
2518
3083
|
function createQueueHandlers(db) {
|
|
2519
3084
|
const sendHandler = async (c) => {
|
|
2520
3085
|
try {
|
|
@@ -2948,6 +3513,14 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
2948
3513
|
app.post("/state/set", stateHandlers.setHandler);
|
|
2949
3514
|
app.post("/state/delete", stateHandlers.deleteHandler);
|
|
2950
3515
|
}
|
|
3516
|
+
if (config.fs) {
|
|
3517
|
+
const dataDir = getDataDir(process.cwd());
|
|
3518
|
+
const fsHandlers = createFsHandlers(dbManager2.emulatorDb, dataDir);
|
|
3519
|
+
app.post("/fs/put", fsHandlers.putHandler);
|
|
3520
|
+
app.post("/fs/get", fsHandlers.getHandler);
|
|
3521
|
+
app.post("/fs/delete", fsHandlers.deleteHandler);
|
|
3522
|
+
app.post("/fs/list", fsHandlers.listHandler);
|
|
3523
|
+
}
|
|
2951
3524
|
if (config.auth) {
|
|
2952
3525
|
const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
|
|
2953
3526
|
app.post("/auth/signup", authHandlers.signupHandler);
|
|
@@ -2979,44 +3552,18 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
2979
3552
|
var DEFAULT_MOCK_SERVER_PORT;
|
|
2980
3553
|
var init_mock_server = __esm({
|
|
2981
3554
|
"../emulator/dist/services/mock-server.js"() {
|
|
3555
|
+
init_paths();
|
|
2982
3556
|
init_auth_service();
|
|
2983
3557
|
init_cache_service();
|
|
2984
3558
|
init_dashboard_routes();
|
|
2985
3559
|
init_db_service();
|
|
3560
|
+
init_fs_service();
|
|
2986
3561
|
init_queue_service();
|
|
2987
3562
|
init_state_service();
|
|
2988
3563
|
init_workflow_service();
|
|
2989
3564
|
DEFAULT_MOCK_SERVER_PORT = 4003;
|
|
2990
3565
|
}
|
|
2991
3566
|
});
|
|
2992
|
-
function getProjectHash(projectDir) {
|
|
2993
|
-
return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
|
|
2994
|
-
}
|
|
2995
|
-
function getTempDir(projectDir) {
|
|
2996
|
-
const hash = getProjectHash(projectDir);
|
|
2997
|
-
return join(tmpdir(), `ploy-emulator-${hash}`);
|
|
2998
|
-
}
|
|
2999
|
-
function getDataDir(projectDir) {
|
|
3000
|
-
return join(projectDir, ".ploy");
|
|
3001
|
-
}
|
|
3002
|
-
function ensureDir(dir) {
|
|
3003
|
-
mkdirSync(dir, { recursive: true });
|
|
3004
|
-
}
|
|
3005
|
-
function ensureTempDir(projectDir) {
|
|
3006
|
-
const tempDir = getTempDir(projectDir);
|
|
3007
|
-
ensureDir(tempDir);
|
|
3008
|
-
return tempDir;
|
|
3009
|
-
}
|
|
3010
|
-
function ensureDataDir(projectDir) {
|
|
3011
|
-
const dataDir = getDataDir(projectDir);
|
|
3012
|
-
ensureDir(dataDir);
|
|
3013
|
-
ensureDir(join(dataDir, "db"));
|
|
3014
|
-
return dataDir;
|
|
3015
|
-
}
|
|
3016
|
-
var init_paths = __esm({
|
|
3017
|
-
"../emulator/dist/utils/paths.js"() {
|
|
3018
|
-
}
|
|
3019
|
-
});
|
|
3020
3567
|
function initializeDatabases(projectDir) {
|
|
3021
3568
|
const dataDir = ensureDataDir(projectDir);
|
|
3022
3569
|
const d1Databases = /* @__PURE__ */ new Map();
|
|
@@ -3158,6 +3705,32 @@ CREATE TABLE IF NOT EXISTS state_entries (
|
|
|
3158
3705
|
value TEXT NOT NULL,
|
|
3159
3706
|
PRIMARY KEY (state_name, key)
|
|
3160
3707
|
);
|
|
3708
|
+
|
|
3709
|
+
-- File storage entries table (metadata for stored files)
|
|
3710
|
+
CREATE TABLE IF NOT EXISTS fs_entries (
|
|
3711
|
+
fs_name TEXT NOT NULL,
|
|
3712
|
+
key TEXT NOT NULL,
|
|
3713
|
+
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
|
3714
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
3715
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
3716
|
+
PRIMARY KEY (fs_name, key)
|
|
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);
|
|
3161
3734
|
`;
|
|
3162
3735
|
}
|
|
3163
3736
|
});
|
|
@@ -3174,6 +3747,7 @@ var init_emulator = __esm({
|
|
|
3174
3747
|
init_env();
|
|
3175
3748
|
init_ploy_config2();
|
|
3176
3749
|
init_workerd_config();
|
|
3750
|
+
init_cron_service();
|
|
3177
3751
|
init_mock_server();
|
|
3178
3752
|
init_queue_service();
|
|
3179
3753
|
init_logger();
|
|
@@ -3189,6 +3763,7 @@ var init_emulator = __esm({
|
|
|
3189
3763
|
workerdProcess = null;
|
|
3190
3764
|
fileWatcher = null;
|
|
3191
3765
|
queueProcessor = null;
|
|
3766
|
+
cronScheduler = null;
|
|
3192
3767
|
resolvedEnvVars = {};
|
|
3193
3768
|
constructor(options = {}) {
|
|
3194
3769
|
const port = options.port ?? 8787;
|
|
@@ -3256,6 +3831,12 @@ var init_emulator = __esm({
|
|
|
3256
3831
|
this.queueProcessor.start();
|
|
3257
3832
|
debug("Queue processor started", this.options.verbose);
|
|
3258
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
|
+
}
|
|
3259
3840
|
success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
|
|
3260
3841
|
log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
|
|
3261
3842
|
if (Object.keys(this.resolvedEnvVars).length > 0) {
|
|
@@ -3270,9 +3851,17 @@ var init_emulator = __esm({
|
|
|
3270
3851
|
if (this.config.cache) {
|
|
3271
3852
|
log(` Cache bindings: ${Object.keys(this.config.cache).join(", ")}`);
|
|
3272
3853
|
}
|
|
3854
|
+
if (this.config.fs) {
|
|
3855
|
+
log(` File Storage bindings: ${Object.keys(this.config.fs).join(", ")}`);
|
|
3856
|
+
}
|
|
3273
3857
|
if (this.config.workflow) {
|
|
3274
3858
|
log(` Workflow bindings: ${Object.keys(this.config.workflow).join(", ")}`);
|
|
3275
3859
|
}
|
|
3860
|
+
if (this.config.cron) {
|
|
3861
|
+
for (const [name, expr] of Object.entries(this.config.cron)) {
|
|
3862
|
+
log(` Cron: ${name} = ${expr}`);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3276
3865
|
this.setupSignalHandlers();
|
|
3277
3866
|
} catch (err) {
|
|
3278
3867
|
error(`Failed to start emulator: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3397,6 +3986,10 @@ var init_emulator = __esm({
|
|
|
3397
3986
|
process.on("SIGTERM", handler);
|
|
3398
3987
|
}
|
|
3399
3988
|
async stop() {
|
|
3989
|
+
if (this.cronScheduler) {
|
|
3990
|
+
this.cronScheduler.stop();
|
|
3991
|
+
this.cronScheduler = null;
|
|
3992
|
+
}
|
|
3400
3993
|
if (this.queueProcessor) {
|
|
3401
3994
|
this.queueProcessor.stop();
|
|
3402
3995
|
this.queueProcessor = null;
|
|
@@ -5333,7 +5926,7 @@ async function typesCommand(options = {}) {
|
|
|
5333
5926
|
console.error("Error: ploy.yaml not found in current directory");
|
|
5334
5927
|
process.exit(1);
|
|
5335
5928
|
}
|
|
5336
|
-
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;
|
|
5337
5930
|
if (!hasBindings2) {
|
|
5338
5931
|
console.log("No bindings found in ploy.yaml. Generating empty Env.");
|
|
5339
5932
|
}
|