@cronvello/sdk 0.1.1 → 0.1.2
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/CHANGELOG.md +44 -0
- package/README.md +162 -11
- package/dist/cli.cjs +877 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +874 -0
- package/dist/cli.js.map +1 -0
- package/dist/{dispatch-handler-Bnda1Ekq.d.cts → dispatch-handler-BNy-H5Nr.d.cts} +41 -2
- package/dist/{dispatch-handler-Bnda1Ekq.d.ts → dispatch-handler-BNy-H5Nr.d.ts} +41 -2
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.cjs +378 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +365 -40
- package/dist/index.js.map +1 -1
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -370,6 +370,109 @@ function enc(segment) {
|
|
|
370
370
|
return encodeURIComponent(segment);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// src/internal/cron.ts
|
|
374
|
+
var MACROS = /* @__PURE__ */ new Set([
|
|
375
|
+
"@yearly",
|
|
376
|
+
"@annually",
|
|
377
|
+
"@monthly",
|
|
378
|
+
"@weekly",
|
|
379
|
+
"@daily",
|
|
380
|
+
"@midnight",
|
|
381
|
+
"@hourly",
|
|
382
|
+
"@reboot"
|
|
383
|
+
]);
|
|
384
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
385
|
+
var DOWS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
386
|
+
var FIVE = [
|
|
387
|
+
{ min: 0, max: 59, label: "minute" },
|
|
388
|
+
{ min: 0, max: 23, label: "hour" },
|
|
389
|
+
{ min: 1, max: 31, label: "day-of-month" },
|
|
390
|
+
{ min: 1, max: 12, names: MONTHS, label: "month" },
|
|
391
|
+
{ min: 0, max: 7, names: DOWS, label: "day-of-week" }
|
|
392
|
+
// 0 and 7 are both Sunday
|
|
393
|
+
];
|
|
394
|
+
var SECONDS = { min: 0, max: 59, label: "second" };
|
|
395
|
+
function validateCron(expr) {
|
|
396
|
+
if (typeof expr !== "string" || !expr.trim()) {
|
|
397
|
+
return { valid: false, error: "schedule is empty" };
|
|
398
|
+
}
|
|
399
|
+
const trimmed = expr.trim();
|
|
400
|
+
if (trimmed.startsWith("@")) {
|
|
401
|
+
return MACROS.has(trimmed.toLowerCase()) ? { valid: true } : { valid: false, error: `unknown cron macro "${trimmed}" (try @daily, @hourly, \u2026)` };
|
|
402
|
+
}
|
|
403
|
+
const parts = trimmed.split(/\s+/);
|
|
404
|
+
if (parts.length !== 5 && parts.length !== 6) {
|
|
405
|
+
return {
|
|
406
|
+
valid: false,
|
|
407
|
+
error: `expected 5 fields (min hour dom month dow) or 6 with seconds, got ${parts.length}: "${trimmed}"`
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const specs = parts.length === 6 ? [SECONDS, ...FIVE] : FIVE;
|
|
411
|
+
for (let i = 0; i < parts.length; i++) {
|
|
412
|
+
const err = validateField(parts[i], specs[i]);
|
|
413
|
+
if (err) return { valid: false, error: `invalid ${specs[i].label} "${parts[i]}": ${err}` };
|
|
414
|
+
}
|
|
415
|
+
return { valid: true };
|
|
416
|
+
}
|
|
417
|
+
function validateField(field2, spec) {
|
|
418
|
+
for (const term of field2.split(",")) {
|
|
419
|
+
const err = validateTerm(term, spec);
|
|
420
|
+
if (err) return err;
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
function validateTerm(term, spec) {
|
|
425
|
+
if (term === "") return "empty list item";
|
|
426
|
+
let step;
|
|
427
|
+
let base = term;
|
|
428
|
+
const slash = term.indexOf("/");
|
|
429
|
+
if (slash >= 0) {
|
|
430
|
+
base = term.slice(0, slash);
|
|
431
|
+
step = term.slice(slash + 1);
|
|
432
|
+
if (!/^\d+$/.test(step) || Number(step) === 0) return `step must be a positive integer`;
|
|
433
|
+
}
|
|
434
|
+
if (base === "*") return null;
|
|
435
|
+
const dash = base.indexOf("-");
|
|
436
|
+
if (dash > 0) {
|
|
437
|
+
const lo = resolveValue(base.slice(0, dash), spec);
|
|
438
|
+
const hi = resolveValue(base.slice(dash + 1), spec);
|
|
439
|
+
if (lo === null) return `"${base.slice(0, dash)}" is out of range ${spec.min}-${spec.max}`;
|
|
440
|
+
if (hi === null) return `"${base.slice(dash + 1)}" is out of range ${spec.min}-${spec.max}`;
|
|
441
|
+
if (lo > hi) return `range start ${lo} is greater than end ${hi}`;
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (step !== void 0 && base !== "*") {
|
|
445
|
+
return resolveValue(base, spec) === null ? `"${base}" is out of range ${spec.min}-${spec.max}` : null;
|
|
446
|
+
}
|
|
447
|
+
return resolveValue(base, spec) === null ? `"${base}" is out of range ${spec.min}-${spec.max}` : null;
|
|
448
|
+
}
|
|
449
|
+
function resolveValue(token, spec) {
|
|
450
|
+
if (/^\d+$/.test(token)) {
|
|
451
|
+
const n = Number(token);
|
|
452
|
+
return n >= spec.min && n <= spec.max ? n : null;
|
|
453
|
+
}
|
|
454
|
+
if (spec.names) {
|
|
455
|
+
const idx = spec.names.indexOf(token.toLowerCase());
|
|
456
|
+
if (idx >= 0) return idx + (spec.label === "month" ? 1 : 0);
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
function assertValidCron(expr, context = "") {
|
|
461
|
+
const res = validateCron(expr);
|
|
462
|
+
if (!res.valid) {
|
|
463
|
+
throw new Error(`${context ? `${context}: ` : ""}invalid cron schedule \u2014 ${res.error}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function isValidTimeZone(tz) {
|
|
467
|
+
if (!tz || typeof tz !== "string") return false;
|
|
468
|
+
try {
|
|
469
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
470
|
+
return true;
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
373
476
|
// src/registry/verify.ts
|
|
374
477
|
var encoder = new TextEncoder();
|
|
375
478
|
function parseBearer(headerValue2) {
|
|
@@ -401,17 +504,54 @@ async function signHmacSha256(payload, secret) {
|
|
|
401
504
|
}
|
|
402
505
|
|
|
403
506
|
// src/registry/dispatch.ts
|
|
507
|
+
var NOOP_LOGGER = {};
|
|
404
508
|
function createDispatcher(state) {
|
|
405
|
-
const log = state.logger;
|
|
509
|
+
const log = state.logger ?? NOOP_LOGGER;
|
|
510
|
+
const hooks = state.hooks;
|
|
511
|
+
const maxBodyBytes = state.maxBodyBytes ?? 1048576;
|
|
512
|
+
async function invokeHandler(job, ctx) {
|
|
513
|
+
await safeHook(() => hooks?.onJobStart?.({ key: job.key, source: ctx.source, isAsync: ctx.isAsync }), log);
|
|
514
|
+
const startedAt = Date.now();
|
|
515
|
+
try {
|
|
516
|
+
const result = await job.config.handler(ctx);
|
|
517
|
+
const durationMs = Date.now() - startedAt;
|
|
518
|
+
log.debug?.(`[cronvello] job '${job.key}' completed in ${durationMs}ms`);
|
|
519
|
+
await safeHook(() => hooks?.onJobSuccess?.({ key: job.key, source: ctx.source, durationMs, result }), log);
|
|
520
|
+
return { durationMs, result };
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const durationMs = Date.now() - startedAt;
|
|
523
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
524
|
+
log.error?.(`[cronvello] job '${job.key}' failed: ${error.message}`, { error: error.message });
|
|
525
|
+
await safeHook(() => hooks?.onJobError?.({ key: job.key, source: ctx.source, durationMs, error }), log);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function buildContext(key, job, req, body, isAsync, source) {
|
|
530
|
+
return {
|
|
531
|
+
key,
|
|
532
|
+
schedule: typeof body["schedule"] === "string" ? body["schedule"] : job.config.schedule ?? "",
|
|
533
|
+
payload: job.config.payload ?? {},
|
|
534
|
+
body,
|
|
535
|
+
headers: req?.headers ?? {},
|
|
536
|
+
isAsync,
|
|
537
|
+
source,
|
|
538
|
+
logger: log,
|
|
539
|
+
signal: req?.signal
|
|
540
|
+
};
|
|
541
|
+
}
|
|
406
542
|
async function handle(req) {
|
|
407
543
|
if (req.method.toUpperCase() !== "POST") {
|
|
408
544
|
return resp(405, { ok: false, error: "Method not allowed" });
|
|
409
545
|
}
|
|
410
546
|
const token = parseBearer(req.authorization);
|
|
411
547
|
if (!token || !timingSafeEqual(token, state.dispatchSecret)) {
|
|
412
|
-
log
|
|
548
|
+
log.warn?.("[cronvello] dispatch rejected: bad or missing bearer token");
|
|
413
549
|
return resp(401, { ok: false, error: "Unauthorized" });
|
|
414
550
|
}
|
|
551
|
+
if (byteLength(req.rawBody) > maxBodyBytes) {
|
|
552
|
+
log.warn?.(`[cronvello] dispatch rejected: body exceeds ${maxBodyBytes} bytes`);
|
|
553
|
+
return resp(413, { ok: false, error: "Payload too large" });
|
|
554
|
+
}
|
|
415
555
|
let body;
|
|
416
556
|
try {
|
|
417
557
|
const parsed = req.rawBody ? JSON.parse(req.rawBody) : {};
|
|
@@ -428,54 +568,44 @@ function createDispatcher(state) {
|
|
|
428
568
|
}
|
|
429
569
|
const job = state.jobs.get(key);
|
|
430
570
|
if (!job) {
|
|
431
|
-
log
|
|
571
|
+
log.warn?.(`[cronvello] dispatch for unknown job '${key}'`);
|
|
432
572
|
return resp(404, { ok: false, error: `Unknown job: ${key}` });
|
|
433
573
|
}
|
|
434
574
|
const callback = readCallback(body["_callback"]);
|
|
435
575
|
const isAsync = callback !== null;
|
|
436
|
-
const ctx =
|
|
437
|
-
key,
|
|
438
|
-
schedule: typeof body["schedule"] === "string" ? body["schedule"] : "",
|
|
439
|
-
payload: job.config.payload ?? {},
|
|
440
|
-
body,
|
|
441
|
-
headers: req.headers,
|
|
442
|
-
isAsync,
|
|
443
|
-
signal: req.signal
|
|
444
|
-
};
|
|
576
|
+
const ctx = buildContext(key, job, req, body, isAsync, "dispatch");
|
|
445
577
|
if (callback) {
|
|
446
|
-
const work = runAndReportCallback(job, ctx, callback, state.dispatchSecret, log);
|
|
578
|
+
const work = runAndReportCallback(job, ctx, callback, state.dispatchSecret, log, invokeHandler);
|
|
447
579
|
if (req.waitUntil) req.waitUntil(work);
|
|
448
580
|
return resp(202, { ok: true, job: key, accepted: true });
|
|
449
581
|
}
|
|
450
|
-
const startedAt = Date.now();
|
|
451
582
|
try {
|
|
452
|
-
const result = await job
|
|
453
|
-
log?.debug?.(`[cronvello] job '${key}' completed in ${Date.now() - startedAt}ms`);
|
|
583
|
+
const { result } = await invokeHandler(job, ctx);
|
|
454
584
|
return resp(200, { ok: true, job: key, result: result ?? null });
|
|
455
585
|
} catch (err) {
|
|
456
|
-
|
|
457
|
-
log?.error?.(`[cronvello] job '${key}' failed: ${message}`, { error: message });
|
|
458
|
-
return resp(500, { ok: false, job: key, error: message });
|
|
586
|
+
return resp(500, { ok: false, job: key, error: err instanceof Error ? err.message : String(err) });
|
|
459
587
|
}
|
|
460
588
|
}
|
|
461
|
-
|
|
589
|
+
async function runLocal(key, payload) {
|
|
590
|
+
const job = state.jobs.get(key);
|
|
591
|
+
if (!job) throw new Error(`Unknown job '${key}'. Known: ${[...state.jobs.keys()].join(", ") || "(none)"}`);
|
|
592
|
+
const body = { job: key, schedule: job.config.schedule, ...payload ?? {} };
|
|
593
|
+
const ctx = buildContext(key, job, null, body, false, "local");
|
|
594
|
+
const { result } = await invokeHandler(job, ctx);
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
return { handle, runLocal };
|
|
462
598
|
}
|
|
463
|
-
async function runAndReportCallback(job, ctx, callback, secret, log) {
|
|
464
|
-
const startedAt = Date.now();
|
|
599
|
+
async function runAndReportCallback(job, ctx, callback, secret, log, invokeHandler) {
|
|
465
600
|
let payload;
|
|
466
601
|
try {
|
|
467
|
-
const result = await job
|
|
468
|
-
payload = {
|
|
469
|
-
runId: callback.runId,
|
|
470
|
-
success: true,
|
|
471
|
-
durationMs: Date.now() - startedAt,
|
|
472
|
-
result: result ?? null
|
|
473
|
-
};
|
|
602
|
+
const { durationMs, result } = await invokeHandler(job, ctx);
|
|
603
|
+
payload = { runId: callback.runId, success: true, durationMs, result: result ?? null };
|
|
474
604
|
} catch (err) {
|
|
475
605
|
payload = {
|
|
476
606
|
runId: callback.runId,
|
|
477
607
|
success: false,
|
|
478
|
-
durationMs:
|
|
608
|
+
durationMs: 0,
|
|
479
609
|
error: err instanceof Error ? err.message : String(err)
|
|
480
610
|
};
|
|
481
611
|
}
|
|
@@ -489,14 +619,26 @@ async function runAndReportCallback(job, ctx, callback, secret, log) {
|
|
|
489
619
|
body: bodyStr
|
|
490
620
|
});
|
|
491
621
|
if (!res.ok) {
|
|
492
|
-
log
|
|
622
|
+
log.warn?.(`[cronvello] callback POST for job '${job.key}' returned ${res.status}`);
|
|
493
623
|
}
|
|
494
624
|
} catch (err) {
|
|
495
|
-
log
|
|
625
|
+
log.error?.(`[cronvello] failed to deliver callback for job '${job.key}'`, {
|
|
496
626
|
error: err instanceof Error ? err.message : String(err)
|
|
497
627
|
});
|
|
498
628
|
}
|
|
499
629
|
}
|
|
630
|
+
async function safeHook(fn, log) {
|
|
631
|
+
try {
|
|
632
|
+
await fn();
|
|
633
|
+
} catch (err) {
|
|
634
|
+
log.warn?.(`[cronvello] a lifecycle hook threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function byteLength(s) {
|
|
638
|
+
if (!s) return 0;
|
|
639
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
|
|
640
|
+
return s.length;
|
|
641
|
+
}
|
|
500
642
|
function readCallback(value) {
|
|
501
643
|
if (!value || typeof value !== "object") return null;
|
|
502
644
|
const v = value;
|
|
@@ -606,9 +748,9 @@ function buildDesiredTask(args) {
|
|
|
606
748
|
function diffTask(existing, desired, opts) {
|
|
607
749
|
const patch = {};
|
|
608
750
|
const changed = [];
|
|
609
|
-
const set = (
|
|
610
|
-
patch[
|
|
611
|
-
changed.push(
|
|
751
|
+
const set = (field2, value) => {
|
|
752
|
+
patch[field2] = value;
|
|
753
|
+
changed.push(field2);
|
|
612
754
|
};
|
|
613
755
|
if (existing.schedule !== desired.schedule) set("schedule", desired.schedule);
|
|
614
756
|
if (desired.timeZone !== void 0 && existing.timeZone !== desired.timeZone) set("timeZone", desired.timeZone);
|
|
@@ -740,10 +882,14 @@ var DEFAULT_DISPATCH_PATH = "/cronvello/dispatch";
|
|
|
740
882
|
var DEFAULT_TIME_ZONE = "Europe/Berlin";
|
|
741
883
|
function defineCronvello(config) {
|
|
742
884
|
validateConfig(config);
|
|
743
|
-
const
|
|
885
|
+
const validate = config.validateSchedules ?? true;
|
|
886
|
+
const defaultTimeZone = config.timeZone ?? DEFAULT_TIME_ZONE;
|
|
887
|
+
if (validate && !isValidTimeZone(defaultTimeZone)) {
|
|
888
|
+
throw new CronvelloConfigError(`Invalid \`timeZone\` "${defaultTimeZone}" \u2014 expected an IANA name like "Europe/Berlin" or "UTC".`);
|
|
889
|
+
}
|
|
890
|
+
const jobs = normalizeJobs(config.jobs, validate);
|
|
744
891
|
const dispatchPath = normalizePath(config.dispatchPath ?? DEFAULT_DISPATCH_PATH);
|
|
745
892
|
const dispatchUrl = joinUrl(config.appUrl, dispatchPath);
|
|
746
|
-
const defaultTimeZone = config.timeZone ?? DEFAULT_TIME_ZONE;
|
|
747
893
|
const client = new CronvelloClient({
|
|
748
894
|
apiKey: config.apiKey,
|
|
749
895
|
baseUrl: config.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL,
|
|
@@ -754,7 +900,9 @@ function defineCronvello(config) {
|
|
|
754
900
|
const dispatcher = createDispatcher({
|
|
755
901
|
jobs,
|
|
756
902
|
dispatchSecret: config.dispatchSecret,
|
|
757
|
-
logger: config.logger
|
|
903
|
+
logger: config.logger,
|
|
904
|
+
...config.hooks ? { hooks: config.hooks } : {},
|
|
905
|
+
...config.maxBodyBytes !== void 0 ? { maxBodyBytes: config.maxBodyBytes } : {}
|
|
758
906
|
});
|
|
759
907
|
const app = {
|
|
760
908
|
client,
|
|
@@ -818,6 +966,7 @@ function defineCronvello(config) {
|
|
|
818
966
|
}
|
|
819
967
|
return client.tasks.runNow(task.id);
|
|
820
968
|
},
|
|
969
|
+
trigger: (key, payload) => dispatcher.runLocal(key, payload),
|
|
821
970
|
handle: dispatcher.handle,
|
|
822
971
|
expressHandler: () => expressHandler(app),
|
|
823
972
|
nextHandler: () => nextHandler(app),
|
|
@@ -825,6 +974,30 @@ function defineCronvello(config) {
|
|
|
825
974
|
};
|
|
826
975
|
return app;
|
|
827
976
|
}
|
|
977
|
+
((defineCronvello2) => {
|
|
978
|
+
function fromEnv(config, env = typeof process !== "undefined" ? process.env : {}) {
|
|
979
|
+
const apiKey = config.apiKey ?? env["CRONVELLO_API_KEY"];
|
|
980
|
+
const dispatchSecret = config.dispatchSecret ?? env["CRONVELLO_DISPATCH_SECRET"];
|
|
981
|
+
const appUrl = config.appUrl ?? env["CRONVELLO_APP_URL"] ?? env["PUBLIC_URL"];
|
|
982
|
+
const baseUrl = config.baseUrl ?? env["CRONVELLO_API_URL"];
|
|
983
|
+
const missing = [
|
|
984
|
+
!apiKey && "CRONVELLO_API_KEY",
|
|
985
|
+
!dispatchSecret && "CRONVELLO_DISPATCH_SECRET",
|
|
986
|
+
!appUrl && "CRONVELLO_APP_URL (or PUBLIC_URL)"
|
|
987
|
+
].filter(Boolean);
|
|
988
|
+
if (missing.length) {
|
|
989
|
+
throw new CronvelloConfigError(`defineCronvello.fromEnv() is missing required env: ${missing.join(", ")}.`);
|
|
990
|
+
}
|
|
991
|
+
return defineCronvello2({
|
|
992
|
+
...config,
|
|
993
|
+
apiKey,
|
|
994
|
+
dispatchSecret,
|
|
995
|
+
appUrl,
|
|
996
|
+
...baseUrl ? { baseUrl } : {}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
defineCronvello2.fromEnv = fromEnv;
|
|
1000
|
+
})(defineCronvello || (defineCronvello = {}));
|
|
828
1001
|
function validateConfig(config) {
|
|
829
1002
|
if (!config) throw new CronvelloConfigError("defineCronvello requires a config object.");
|
|
830
1003
|
if (!config.appName || !config.appName.trim()) throw new CronvelloConfigError("`appName` is required.");
|
|
@@ -837,7 +1010,7 @@ function validateConfig(config) {
|
|
|
837
1010
|
}
|
|
838
1011
|
if (!config.jobs) throw new CronvelloConfigError("`jobs` is required.");
|
|
839
1012
|
}
|
|
840
|
-
function normalizeJobs(input) {
|
|
1013
|
+
function normalizeJobs(input, validate) {
|
|
841
1014
|
const map = /* @__PURE__ */ new Map();
|
|
842
1015
|
const entries = Array.isArray(input) ? input.map(({ key, ...rest }) => ({ key, config: rest })) : Object.entries(input).map(([key, config]) => ({ key, config }));
|
|
843
1016
|
for (const { key, config } of entries) {
|
|
@@ -851,6 +1024,15 @@ function normalizeJobs(input) {
|
|
|
851
1024
|
if (!config.schedule || !config.schedule.trim()) {
|
|
852
1025
|
throw new CronvelloConfigError(`Job '${trimmed}' is missing a schedule.`);
|
|
853
1026
|
}
|
|
1027
|
+
if (validate) {
|
|
1028
|
+
const cronCheck = validateCron(config.schedule);
|
|
1029
|
+
if (!cronCheck.valid) {
|
|
1030
|
+
throw new CronvelloConfigError(`Job '${trimmed}' has an invalid schedule "${config.schedule}" \u2014 ${cronCheck.error}.`);
|
|
1031
|
+
}
|
|
1032
|
+
if (config.timeZone !== void 0 && !isValidTimeZone(config.timeZone)) {
|
|
1033
|
+
throw new CronvelloConfigError(`Job '${trimmed}' has an invalid timeZone "${config.timeZone}" \u2014 expected an IANA name.`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
854
1036
|
map.set(trimmed, { key: trimmed, config });
|
|
855
1037
|
}
|
|
856
1038
|
if (map.size === 0) throw new CronvelloConfigError("At least one job is required.");
|
|
@@ -893,6 +1075,149 @@ function buildRegistryRequest(config, dispatchUrl, defaultTimeZone, jobs, opts)
|
|
|
893
1075
|
};
|
|
894
1076
|
}
|
|
895
1077
|
|
|
1078
|
+
// src/registry/format.ts
|
|
1079
|
+
var ESC = String.fromCharCode(27);
|
|
1080
|
+
var ANSI = {
|
|
1081
|
+
reset: `${ESC}[0m`,
|
|
1082
|
+
dim: `${ESC}[2m`,
|
|
1083
|
+
bold: `${ESC}[1m`,
|
|
1084
|
+
green: `${ESC}[32m`,
|
|
1085
|
+
yellow: `${ESC}[33m`,
|
|
1086
|
+
red: `${ESC}[31m`,
|
|
1087
|
+
cyan: `${ESC}[36m`,
|
|
1088
|
+
gray: `${ESC}[90m`
|
|
1089
|
+
};
|
|
1090
|
+
var GLYPH = {
|
|
1091
|
+
created: { sign: "+", color: "green" },
|
|
1092
|
+
updated: { sign: "~", color: "yellow" },
|
|
1093
|
+
unchanged: { sign: "=", color: "gray" },
|
|
1094
|
+
deleted: { sign: "-", color: "red" },
|
|
1095
|
+
skipped: { sign: "\xB7", color: "cyan" }
|
|
1096
|
+
};
|
|
1097
|
+
function formatSyncResult(result, options = {}) {
|
|
1098
|
+
const paint = (s, c) => options.color ? `${ANSI[c]}${s}${ANSI.reset}` : s;
|
|
1099
|
+
const verb = options.dryRun ? "would sync" : "synced";
|
|
1100
|
+
const lines = [];
|
|
1101
|
+
lines.push(
|
|
1102
|
+
`${paint("Cronvello", "cyan")} ${verb} ${paint(`"${result.jobName}"`, "bold")} ${paint(`(${result.jobId})`, "gray")}`
|
|
1103
|
+
);
|
|
1104
|
+
const order = ["created", "updated", "deleted", "skipped", "unchanged"];
|
|
1105
|
+
const sorted = [...result.changes].sort((a, b) => order.indexOf(a.action) - order.indexOf(b.action));
|
|
1106
|
+
for (const c of sorted) {
|
|
1107
|
+
const g = GLYPH[c.action];
|
|
1108
|
+
const detail = c.changedFields && c.changedFields.length ? paint(` (${c.changedFields.join(", ")})`, "gray") : c.reason ? paint(` (${c.reason})`, "gray") : "";
|
|
1109
|
+
lines.push(` ${paint(g.sign, g.color)} ${paint(c.action.padEnd(9), g.color)} ${c.key}${detail}`);
|
|
1110
|
+
}
|
|
1111
|
+
const tally = [
|
|
1112
|
+
result.created && `${result.created} created`,
|
|
1113
|
+
result.updated && `${result.updated} updated`,
|
|
1114
|
+
result.unchanged && `${result.unchanged} unchanged`,
|
|
1115
|
+
result.deleted && `${result.deleted} deleted`,
|
|
1116
|
+
result.skipped && `${result.skipped} skipped`
|
|
1117
|
+
].filter(Boolean);
|
|
1118
|
+
const summary = tally.length ? tally.join(", ") : "no changes";
|
|
1119
|
+
const containerNote = result.jobCreated ? paint(" \xB7 container created", "gray") : "";
|
|
1120
|
+
lines.push(` ${paint(summary, "bold")}${containerNote}`);
|
|
1121
|
+
return lines.join("\n");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/schedule/index.ts
|
|
1125
|
+
var DOW = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
1126
|
+
function cron(expression) {
|
|
1127
|
+
assertValidCron(expression, "cron()");
|
|
1128
|
+
return expression;
|
|
1129
|
+
}
|
|
1130
|
+
function every(interval) {
|
|
1131
|
+
const m = /^(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|hour|hours|d|day|days)$/i.exec(interval.trim());
|
|
1132
|
+
if (!m) throw new Error(`every(): cannot parse interval "${interval}" \u2014 use forms like "30s", "15m", "2h", "1d"`);
|
|
1133
|
+
const n = Number(m[1]);
|
|
1134
|
+
const unit = m[2].toLowerCase()[0];
|
|
1135
|
+
if (n < 1) throw new Error(`every(): interval must be at least 1, got ${n}`);
|
|
1136
|
+
let expr;
|
|
1137
|
+
if (unit === "s") {
|
|
1138
|
+
if (n > 59) throw new Error(`every(): seconds must be 1\u201359, got ${n}`);
|
|
1139
|
+
expr = `*/${n} * * * * *`;
|
|
1140
|
+
} else if (unit === "m") {
|
|
1141
|
+
if (n > 59) throw new Error(`every(): minutes must be 1\u201359 (use "1h" for 60), got ${n}`);
|
|
1142
|
+
expr = `*/${n} * * * *`;
|
|
1143
|
+
} else if (unit === "h") {
|
|
1144
|
+
if (n > 23) throw new Error(`every(): hours must be 1\u201323 (use "1d" for 24), got ${n}`);
|
|
1145
|
+
expr = `0 */${n} * * *`;
|
|
1146
|
+
} else {
|
|
1147
|
+
if (n > 31) throw new Error(`every(): days must be 1\u201331, got ${n}`);
|
|
1148
|
+
expr = n === 1 ? `0 0 * * *` : `0 0 */${n} * *`;
|
|
1149
|
+
}
|
|
1150
|
+
assertValidCron(expr, `every("${interval}")`);
|
|
1151
|
+
return expr;
|
|
1152
|
+
}
|
|
1153
|
+
function everyMinutes(n) {
|
|
1154
|
+
return every(`${n}m`);
|
|
1155
|
+
}
|
|
1156
|
+
function everyHours(n) {
|
|
1157
|
+
return every(`${n}h`);
|
|
1158
|
+
}
|
|
1159
|
+
function hourly(minute = 0) {
|
|
1160
|
+
const mm = field(minute, 0, 59, "minute");
|
|
1161
|
+
return cron(`${mm} * * * *`);
|
|
1162
|
+
}
|
|
1163
|
+
function daily(time = "00:00") {
|
|
1164
|
+
const { hh, mm } = parseTime(time);
|
|
1165
|
+
return cron(`${mm} ${hh} * * *`);
|
|
1166
|
+
}
|
|
1167
|
+
function weekly(day, time = "00:00") {
|
|
1168
|
+
const { hh, mm } = parseTime(time);
|
|
1169
|
+
return cron(`${mm} ${hh} * * ${weekday(day)}`);
|
|
1170
|
+
}
|
|
1171
|
+
function monthly(dayOfMonth, time = "00:00") {
|
|
1172
|
+
const { hh, mm } = parseTime(time);
|
|
1173
|
+
const dom = field(dayOfMonth, 1, 31, "day-of-month");
|
|
1174
|
+
return cron(`${mm} ${hh} ${dom} * *`);
|
|
1175
|
+
}
|
|
1176
|
+
function weekdays(time = "00:00") {
|
|
1177
|
+
const { hh, mm } = parseTime(time);
|
|
1178
|
+
return cron(`${mm} ${hh} * * 1-5`);
|
|
1179
|
+
}
|
|
1180
|
+
function weekends(time = "00:00") {
|
|
1181
|
+
const { hh, mm } = parseTime(time);
|
|
1182
|
+
return cron(`${mm} ${hh} * * 0,6`);
|
|
1183
|
+
}
|
|
1184
|
+
var schedule = {
|
|
1185
|
+
cron,
|
|
1186
|
+
every,
|
|
1187
|
+
everyMinutes,
|
|
1188
|
+
everyHours,
|
|
1189
|
+
hourly,
|
|
1190
|
+
daily,
|
|
1191
|
+
weekly,
|
|
1192
|
+
monthly,
|
|
1193
|
+
weekdays,
|
|
1194
|
+
weekends
|
|
1195
|
+
};
|
|
1196
|
+
function parseTime(time) {
|
|
1197
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
1198
|
+
if (!m) throw new Error(`Expected a time like "08:00" or "23:30", got "${time}"`);
|
|
1199
|
+
const hh = Number(m[1]);
|
|
1200
|
+
const mm = Number(m[2]);
|
|
1201
|
+
if (hh > 23) throw new Error(`Hour must be 0\u201323, got ${hh}`);
|
|
1202
|
+
if (mm > 59) throw new Error(`Minute must be 0\u201359, got ${mm}`);
|
|
1203
|
+
return { hh, mm };
|
|
1204
|
+
}
|
|
1205
|
+
function weekday(day) {
|
|
1206
|
+
if (typeof day === "number") {
|
|
1207
|
+
if (day < 0 || day > 6) throw new Error(`Weekday number must be 0 (Sun) \u2013 6 (Sat), got ${day}`);
|
|
1208
|
+
return day;
|
|
1209
|
+
}
|
|
1210
|
+
const n = DOW[day.toLowerCase()];
|
|
1211
|
+
if (n === void 0) throw new Error(`Unknown weekday "${day}" \u2014 use sun\u2026sat or 0\u20266`);
|
|
1212
|
+
return n;
|
|
1213
|
+
}
|
|
1214
|
+
function field(value, min, max, label) {
|
|
1215
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
1216
|
+
throw new Error(`${label} must be an integer ${min}\u2013${max}, got ${value}`);
|
|
1217
|
+
}
|
|
1218
|
+
return value;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
896
1221
|
// src/index.ts
|
|
897
1222
|
function generateDispatchSecret(bytes = 32) {
|
|
898
1223
|
const buf = new Uint8Array(bytes);
|
|
@@ -900,6 +1225,6 @@ function generateDispatchSecret(bytes = 32) {
|
|
|
900
1225
|
return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
901
1226
|
}
|
|
902
1227
|
|
|
903
|
-
export { CRONVELLO_DEFAULT_BASE_URL, CronvelloApiError, CronvelloClient, CronvelloConfigError, CronvelloError, CronvelloNetworkError, defineCronvello, generateDispatchSecret };
|
|
1228
|
+
export { CRONVELLO_DEFAULT_BASE_URL, CronvelloApiError, CronvelloClient, CronvelloConfigError, CronvelloError, CronvelloNetworkError, cron, daily, defineCronvello, every, everyHours, everyMinutes, formatSyncResult, generateDispatchSecret, hourly, isValidTimeZone, monthly, schedule, validateCron, weekdays, weekends, weekly };
|
|
904
1229
|
//# sourceMappingURL=index.js.map
|
|
905
1230
|
//# sourceMappingURL=index.js.map
|