@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 ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@cronvello/sdk` are documented here. This project adheres to
4
+ [Semantic Versioning](https://semver.org/) (pre-1.0: minor-feature additions ship as patch releases).
5
+
6
+ ## 0.1.2
7
+
8
+ A big developer-experience release — everything is additive and backward-compatible.
9
+
10
+ ### Added
11
+
12
+ - **Readable schedule builders**: `every`, `everyMinutes`, `everyHours`, `hourly`, `daily`,
13
+ `weekly`, `monthly`, `weekdays`, `weekends`, and `cron` — each returns a validated cron string.
14
+ - **Client-side validation** of cron expressions and IANA timezones at define time
15
+ (`validateCron`, `isValidTimeZone`); a typo now throws a clear, job-scoped error instead of
16
+ failing silently server-side. Opt out per app with `validateSchedules: false`.
17
+ - **`cronvello.trigger(key, payload?)`** — run a job's handler in-process (no HTTP, no deploy).
18
+ Ideal for unit tests and local development. `ctx.source` is `"local"` for these runs.
19
+ - **`defineCronvello.fromEnv(config)`** — read `CRONVELLO_API_KEY`, `CRONVELLO_DISPATCH_SECRET`,
20
+ and `CRONVELLO_APP_URL` (or `PUBLIC_URL`) from the environment so you don't repeat them.
21
+ - **Lifecycle hooks** (`onJobStart`, `onJobSuccess`, `onJobError`) for logging, metrics, and error
22
+ reporting. A throwing hook is logged and never breaks the run.
23
+ - **`ctx.logger`** (your configured logger, or a no-op) and **`ctx.source`** on the handler context.
24
+ - **`formatSyncResult(result, { color?, dryRun? })`** — a clean, human-readable reconcile summary.
25
+ - **CLI** — `npx cronvello` with `whoami`, `list`, `tasks`, `runs`, `run`, `status`, `sync`,
26
+ `dev`, and `secret`. Polished, colour-aware output; `--json` for scripting.
27
+ - **Dispatch body-size guard** (`maxBodyBytes`, default 1 MiB) — oversized inbound bodies are
28
+ rejected with `413` before parsing.
29
+
30
+ ### Internal
31
+
32
+ - Comprehensive test suite (unit + contract + opt-in live e2e) and a CI gate
33
+ (typecheck + tests + build across Node 18/20/22) wired ahead of publish.
34
+
35
+ ## 0.1.1
36
+
37
+ - **Fix**: unwrap the server's `{ success, message, data }` response envelope centrally in the
38
+ transport, so `sync()` and the typed client methods see the inner payload (the 0.1.0 release
39
+ returned the wrapper).
40
+
41
+ ## 0.1.0
42
+
43
+ - Initial release: code-first registry (`defineCronvello` → `sync()` + Express/Next adapters) and
44
+ a typed low-level `/v1` client. Zero runtime dependencies, dual ESM + CJS.
package/README.md CHANGED
@@ -13,6 +13,11 @@ npm i @cronvello/sdk
13
13
  - ✅ **Base URL baked in** (`https://api.cronvello.com`) — override only for self-hosting.
14
14
  - ✅ **Two layers**: a turnkey code-first **registry**, and a typed **low-level client** for the
15
15
  full `/v1` API.
16
+ - ✅ **Readable schedules** — `daily("08:00")`, `every("15m")`, `weekly("mon", "09:00")` instead of
17
+ raw cron — with **typo-proof validation** at define time.
18
+ - ✅ **Test jobs locally** with `cronvello.trigger("job")` — no deploy, no HTTP.
19
+ - ✅ **Lifecycle hooks** for logging / metrics / error reporting.
20
+ - ✅ **A real CLI** — `npx cronvello whoami | list | runs | status | sync | dev`.
16
21
 
17
22
  ---
18
23
 
@@ -30,26 +35,26 @@ Change a schedule → it's updated. Idempotent, every time.
30
35
 
31
36
  ```ts
32
37
  // cronvello.ts
33
- import { defineCronvello } from "@cronvello/sdk";
38
+ import { defineCronvello, daily, every } from "@cronvello/sdk";
34
39
 
35
- export const cronvello = defineCronvello({
36
- appName: "my-app", // your app's identity (stable, unique)
37
- appUrl: process.env.APP_URL!, // public URL of THIS app, e.g. https://my-app.com
38
- apiKey: process.env.CRONVELLO_API_KEY!, // crn_live_…
39
- dispatchSecret: process.env.CRONVELLO_DISPATCH_SECRET!, // random 32-byte secret
40
+ // `fromEnv` reads CRONVELLO_API_KEY, CRONVELLO_DISPATCH_SECRET, and CRONVELLO_APP_URL
41
+ // (or PUBLIC_URL) for you — pass them explicitly instead if you prefer.
42
+ export const cronvello = defineCronvello.fromEnv({
43
+ appName: "my-app", // your app's identity (stable, unique)
40
44
 
41
45
  jobs: {
42
46
  "send-daily-digest": {
43
- schedule: "0 8 * * *",
47
+ schedule: daily("08:00"), // ← readable; or a raw "0 8 * * *"
44
48
  handler: async () => {
45
49
  await sendDigests();
46
50
  },
47
51
  },
48
52
  "cleanup-temp": {
49
- schedule: "*/15 * * * *",
53
+ schedule: every("15m"),
50
54
  description: "Purge temp files older than an hour",
51
- handler: async ({ schedule }) => {
55
+ handler: async ({ schedule, logger }) => {
52
56
  const removed = await purgeTemp();
57
+ logger.info?.(`purged ${removed} files`);
53
58
  return { removed, schedule }; // returned value is recorded on the run
54
59
  },
55
60
  },
@@ -120,14 +125,16 @@ jobs: [...billingJobs, ...reportJobs] // each: { key, schedule, handler, … }
120
125
 
121
126
  | Field | Default | Notes |
122
127
  |---|---|---|
123
- | `schedule` | — | Cron expression, validated by Cronvello on sync. |
128
+ | `schedule` | — | Cron string or a [schedule builder](#readable-schedules). Validated client-side at define time, and again by Cronvello on sync. |
124
129
  | `handler` | — | `async (ctx) => result`. The return value is recorded on the run. |
125
130
  | `description` | — | Stored on the task. |
126
- | `timeZone` | app default (`Europe/Berlin`) | IANA zone. |
131
+ | `timeZone` | app default (`Europe/Berlin`) | IANA zone. Validated client-side. |
127
132
  | `urgency` | — | `low` \| `medium` \| `high` \| `critical`. |
128
133
  | `maxRetries` | server default | 0–20. |
129
134
  | `executionMode` | `sync` | `async_callback` for long jobs (see below). |
135
+ | `callbackTimeoutMs` | — | Async-mode budget before Cronvello marks the run timed out. |
130
136
  | `allowConcurrentRuns` | `false` | Allow overlap with an in-flight run. |
137
+ | `successCriteria` | — | Assert success beyond 2xx (status range, body contains / JSON path / regex). |
131
138
  | `payload` | — | Static object merged into the request body and exposed as `ctx.payload`. |
132
139
  | `enabled` | `true` | `false` keeps the code but stops scheduling. |
133
140
 
@@ -140,11 +147,102 @@ handler: async (ctx) => {
140
147
  ctx.payload; // your static payload, if any
141
148
  ctx.body; // full request body Cronvello sent
142
149
  ctx.isAsync; // true under async_callback mode
150
+ ctx.source; // "dispatch" (Cronvello fired it) | "local" (you called trigger())
151
+ ctx.logger; // your configured logger (no-op if none) — ctx.logger.info?.("…")
152
+ ctx.signal; // AbortSignal, if the host provides one
143
153
  };
144
154
  ```
145
155
 
146
156
  ---
147
157
 
158
+ ## Readable schedules
159
+
160
+ Stop hand-writing cron. These builders return a validated cron string, so a bad argument throws
161
+ **at define time** with a clear message — not silently at the server:
162
+
163
+ ```ts
164
+ import { every, everyMinutes, everyHours, hourly, daily, weekly, monthly, weekdays, weekends, cron } from "@cronvello/sdk";
165
+
166
+ every("30s") // "*/30 * * * * *"
167
+ every("15m") // "*/15 * * * *"
168
+ every("2h") // "0 */2 * * *"
169
+ hourly() // "0 * * * *" — top of every hour
170
+ hourly(30) // "30 * * * *"
171
+ daily("08:00") // "0 8 * * *"
172
+ weekly("mon", "09:00") // "0 9 * * 1"
173
+ monthly(1, "00:00") // "0 0 1 * *" — 1st of the month
174
+ weekdays("07:00") // "0 7 * * 1-5" — Mon–Fri
175
+ weekends("10:00") // "0 10 * * 0,6"
176
+ cron("0 6,7 * * *") // raw cron, still validated
177
+ ```
178
+
179
+ Validation runs on every job's `schedule` and `timeZone` when you call `defineCronvello`. Opt out
180
+ with `validateSchedules: false`. You can also validate directly:
181
+
182
+ ```ts
183
+ import { validateCron, isValidTimeZone } from "@cronvello/sdk";
184
+ validateCron("0 25 * * *"); // { valid: false, error: 'invalid hour "25": …' }
185
+ isValidTimeZone("Europe/Berlin"); // true
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Test & run jobs locally — `trigger()`
191
+
192
+ Run a job's handler **in-process** — no HTTP, no Cronvello, no deploy. Perfect for unit tests and
193
+ local development:
194
+
195
+ ```ts
196
+ const result = await cronvello.trigger("send-daily-digest");
197
+ // runs the handler with ctx.source === "local"; lifecycle hooks fire; returns the handler's value
198
+ ```
199
+
200
+ ```ts
201
+ // In a test:
202
+ it("sends digests", async () => {
203
+ const out = await cronvello.trigger("send-daily-digest", { dryRun: true });
204
+ expect(out).toEqual({ sent: 3 });
205
+ });
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Lifecycle hooks & logging
211
+
212
+ Observe every run — for structured logs, metrics, or error reporting (Sentry, etc.). Hooks never
213
+ break a run: if a hook throws, it's logged and the job still completes.
214
+
215
+ ```ts
216
+ defineCronvello.fromEnv({
217
+ appName: "my-app",
218
+ logger: console, // surfaced to handlers as ctx.logger
219
+ hooks: {
220
+ onJobStart: ({ key, source }) => metrics.increment(`cron.start`, { key }),
221
+ onJobSuccess: ({ key, durationMs }) => metrics.timing(`cron.ms`, durationMs, { key }),
222
+ onJobError: ({ key, error }) => Sentry.captureException(error, { tags: { key } }),
223
+ },
224
+ jobs: { /* … */ },
225
+ });
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Pretty sync output
231
+
232
+ ```ts
233
+ import { formatSyncResult } from "@cronvello/sdk";
234
+
235
+ const result = await cronvello.sync();
236
+ console.log(formatSyncResult(result, { color: true }));
237
+ // Cronvello synced "my-app" (job_…)
238
+ // + created send-daily-digest
239
+ // ~ updated cleanup-temp (schedule)
240
+ // = unchanged rotate-keys
241
+ // 1 created, 1 updated, 1 unchanged
242
+ ```
243
+
244
+ ---
245
+
148
246
  ## `sync()` — idempotent reconcile
149
247
 
150
248
  ```ts
@@ -211,6 +309,59 @@ long-running Node server — **not** typical serverless).
211
309
 
212
310
  ---
213
311
 
312
+ ## CLI — `npx cronvello`
313
+
314
+ Installing the SDK gives you a `cronvello` command. Point it at your account with
315
+ `CRONVELLO_API_KEY` (a `crn_live_…` key) and inspect or drive everything from the terminal:
316
+
317
+ ```bash
318
+ export CRONVELLO_API_KEY=crn_live_…
319
+
320
+ npx cronvello whoami # account, plan, limits & usage
321
+ npx cronvello list # job containers (tasks / active / errors)
322
+ npx cronvello list "my-app" # the tasks inside one container
323
+ npx cronvello tasks --job "my-app" # account-wide task table
324
+ npx cronvello runs --limit 20 # recent execution feed
325
+ npx cronvello runs --status failed # …filtered
326
+ npx cronvello run <taskId> # trigger a task now
327
+ npx cronvello run "my-app" "digest" # …resolved by job + task name
328
+ npx cronvello status # health overview + recent failures
329
+ npx cronvello secret # generate a strong dispatch secret
330
+ ```
331
+
332
+ ```
333
+ $ cronvello whoami
334
+ ╭─ System (internal siblings) ───────────────────╮
335
+ │ account #1 ✔ system@node-cron.internal │
336
+ │ plan Enterprise │
337
+ │ limits 1000 jobs · 100 tasks/job · 6000/min │
338
+ │ usage 236253 / ∞ executions (6 jobs) │
339
+ │ attention 4 DLQ · 0 heartbeats · 0 maintenance │
340
+ ╰────────────────────────────────────────────────╯
341
+ ```
342
+
343
+ **Drive your code registry**, too — these load your config module (a file that exports a
344
+ `defineCronvello(...)` app as the default export or a named `cronvello`):
345
+
346
+ ```bash
347
+ npx cronvello sync ./cronvello.config.js # reconcile to Cronvello
348
+ npx cronvello sync ./cronvello.config.js --dry # preview the diff
349
+ npx cronvello dev ./cronvello.config.js my-job # run a job's handler locally
350
+ ```
351
+
352
+ > Running a **TypeScript** config? Launch the CLI under a TS loader:
353
+ > `node --import tsx node_modules/@cronvello/sdk/dist/cli.js sync ./cronvello.config.ts`
354
+
355
+ Global flags: `--json` (machine-readable output for scripting), `--no-color`, `-h/--help`,
356
+ `-v/--version`. Colour auto-disables when piped or when `NO_COLOR` is set.
357
+
358
+ ---
359
+
360
+ Runnable examples live in [`examples/`](./examples): a full Express app (`examples/express`) and a
361
+ Next.js App Router setup (`examples/next`).
362
+
363
+ ---
364
+
214
365
  ## Low-level client
215
366
 
216
367
  The full typed `/v1` surface, for anything beyond the registry model: