@cronvello/sdk 0.1.4 → 0.2.1

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 CHANGED
@@ -3,6 +3,59 @@
3
3
  All notable changes to `@cronvello/sdk` are documented here. This project adheres to
4
4
  [Semantic Versioning](https://semver.org/) (pre-1.0: minor-feature additions ship as patch releases).
5
5
 
6
+ ## 0.2.1
7
+
8
+ - Move the canonical SDK source to the public
9
+ [`niccasWilliams/cronvello-sdk`](https://github.com/niccasWilliams/cronvello-sdk) repository.
10
+ - Add public CI and a tokenless npm Trusted Publishing workflow with automatic provenance.
11
+ - Replace internal production-derived contract examples with synthetic fixtures of the same wire
12
+ shape. Runtime behavior and the public API are unchanged.
13
+
14
+ ## 0.2.0
15
+
16
+ **The SDK now runs locally with no account.** Until now `@cronvello/sdk` was cloud-coupled: jobs
17
+ only ran once `sync()` had registered them and Cronvello called your app back. This release adds a
18
+ real **local execution engine** and a small **local dashboard**, so the SDK is useful on its own —
19
+ `npx cronvello dev` starts a scheduler loop on your machine and actually fires your handlers when
20
+ they're due, and `--dashboard` opens a live web UI for them. No account, no network. Everything here
21
+ is additive; the cloud `sync()` / dispatch paths are unchanged.
22
+
23
+ ### Added
24
+
25
+ - **Local engine** — a from-scratch, zero-dependency scheduler. For each job it computes the next
26
+ fire time from its cron expression (5- or 6-field, `@macros`, ranges/lists/steps/names) and runs
27
+ the handler when due, enforcing **timezone/DST**, **overlap protection** (`allowConcurrentRuns`),
28
+ **per-run timeout** (from `callbackTimeoutMs`), and **retry with exponential backoff**
29
+ (`maxRetries`) — locally. Keeps an in-memory run-history ring buffer and shuts down cleanly.
30
+ - **`cronvello dev [entry]`** — start the local engine from your config module, with a live job
31
+ table and run feed. `--dry-run [--window <dur>]` prints what would fire in the next window
32
+ without executing anything.
33
+ - **`cronvello preview "<cron>" [--tz <IANA>] [-n 5]`** — print the next N fire times of any cron
34
+ expression in a timezone. Also available programmatically as `previewSchedule(expr, opts)`.
35
+ - **`app.dev(options?)`** on the object returned by `defineCronvello` — starts the local engine and
36
+ returns its handle (`runs()`, `snapshot()`, `stop()`).
37
+ - **`@cronvello/sdk/dev` subpath export** — the engine primitives (`createLocalEngine`,
38
+ `nextOccurrence`, `previewSchedule`, `upcomingFires`, `startDashboard`, types). `nextOccurrence`
39
+ and `previewSchedule` are re-exported from the main entry too.
40
+ - **`ctx.signal` on local runs** — the engine aborts it when a run exceeds its timeout, so
41
+ cooperative handlers can cancel their work.
42
+ - **Local dashboard** — `cronvello dev --dashboard [--port N]` (or `app.dev({ dashboard: true })`)
43
+ serves a small, zero-dependency web UI over the running engine on `127.0.0.1`: a job table with a
44
+ live next-fire countdown, a Server-Sent-Events run feed (fire/success/error/timeout/retry/skip), a
45
+ per-job drawer with recent runs and upcoming fire times, and a **Run now** button. Read-only JSON
46
+ API plus `GET /api/runs.ndjson` to export the run history. Light + dark, no framework, no network,
47
+ MIT. Mutating requests are same-origin-guarded. The dashboard code is loaded lazily, so it never
48
+ weighs down the main bundle.
49
+ - **`engine.subscribe(listener)`**, **`engine.onStop(hook)`**, **`engine.trigger(key)`**, and
50
+ **`engine.toNdjson()`** on `LocalEngine` — the multi-subscriber, manual-run, and export primitives
51
+ the dashboard is built on (`engine.stop()` also tears the dashboard down).
52
+
53
+ ### Changed
54
+
55
+ - **`cronvello dev <jobKey>` (single-shot) is now `cronvello trigger <jobKey>`** (pre-1.0 rename).
56
+ `cronvello dev` now starts the local engine instead of running one job once. `app.trigger()` is
57
+ unchanged.
58
+
6
59
  ## 0.1.4
7
60
 
8
61
  - **Fix the `cronvello` CLI when launched via the `bin` symlink.** The entry-point check compared
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @cronvello/sdk
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@cronvello/sdk)](https://www.npmjs.com/package/@cronvello/sdk)
4
+ [![CI](https://github.com/niccasWilliams/cronvello-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/niccasWilliams/cronvello-sdk/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@cronvello/sdk)](./LICENSE)
6
+
3
7
  **Code-first cron jobs for [Cronvello](https://cronvello.com).** Define your scheduled jobs in
4
8
  your codebase, and the SDK keeps them in sync with Cronvello and runs them — **no dashboard
5
9
  required**. Your jobs always follow your code.
@@ -8,6 +12,8 @@ required**. Your jobs always follow your code.
8
12
  npm i @cronvello/sdk
9
13
  ```
10
14
 
15
+ - ✅ **Runs locally with no account** — `npx cronvello dev` starts a real scheduler loop on your
16
+ machine (timezone/DST, overlap protection, timeouts, retry/backoff). No cloud, no network.
11
17
  - ✅ **Zero runtime dependencies** (Node 20+ `fetch` + WebCrypto).
12
18
  - ✅ **Dual ESM + CJS**, full TypeScript types.
13
19
  - ✅ **Base URL baked in** (`https://api.cronvello.com`) — override only for self-hosting.
@@ -17,7 +23,7 @@ npm i @cronvello/sdk
17
23
  raw cron — with **typo-proof validation** at define time.
18
24
  - ✅ **Test jobs locally** with `cronvello.trigger("job")` — no deploy, no HTTP.
19
25
  - ✅ **Lifecycle hooks** for logging / metrics / error reporting.
20
- - ✅ **A real CLI** — `npx cronvello whoami | list | runs | status | sync | dev`.
26
+ - ✅ **A real CLI** — `npx cronvello dev | preview | whoami | list | runs | status | sync`.
21
27
 
22
28
  ---
23
29
 
@@ -207,6 +213,134 @@ it("sends digests", async () => {
207
213
 
208
214
  ---
209
215
 
216
+ ## Local development (no account) — `cronvello dev`
217
+
218
+ You don't need a Cronvello account, an API key, or a deploy to run your jobs. **The SDK ships a real
219
+ local engine.** Point the CLI at the module that exports your app and it starts a scheduler loop on
220
+ your machine that actually fires the handlers when they're due — computing each next fire time from
221
+ its cron expression (timezone- and DST-aware) and enforcing the same production policies the cloud
222
+ does: **overlap protection, per-run timeout, and retry with backoff**. No cloud, no network.
223
+
224
+ ```ts
225
+ // cronvello.config.ts
226
+ import { defineCronvello, every, daily } from "@cronvello/sdk";
227
+
228
+ export const cronvello = defineCronvello({
229
+ appName: "my-app",
230
+ appUrl: "https://my-app.example.com",
231
+ apiKey: "crn_local_dev", // any non-empty value — the local engine never calls the API
232
+ dispatchSecret: "local-dev-secret-0123456789abcdef",
233
+ timeZone: "Europe/Berlin",
234
+ jobs: {
235
+ heartbeat: { schedule: every("10s"), handler: async () => { console.log("beat"); } },
236
+ "daily-digest": { schedule: daily("08:00"), handler: async () => sendDigests() },
237
+ },
238
+ });
239
+ ```
240
+
241
+ ```bash
242
+ npx cronvello dev # auto-detects cronvello.config.{ts,js,mjs} (or pass a path)
243
+ ```
244
+
245
+ ```
246
+ ◷ Cronvello dev · local engine · no account, no cloud
247
+
248
+ JOB SCHEDULE TZ NEXT RUN WHEN
249
+ heartbeat */10 * * * * * Europe/Berlin 2026-06-28 18:03:10 in 8s
250
+ daily-digest 0 8 * * * Europe/Berlin 2026-06-29 08:00:00 in 14h
251
+
252
+ watching 2 job(s) — press Ctrl-C to stop
253
+
254
+ 18:03:10 → heartbeat fired
255
+ 18:03:10 ✔ heartbeat 3ms
256
+ 18:03:20 → heartbeat fired
257
+ 18:03:20 ✔ heartbeat 2ms
258
+ ```
259
+
260
+ Every fire is logged with its result, duration, retries, and any timeouts; the engine keeps an
261
+ in-memory history of recent runs and shuts down cleanly on `Ctrl-C`. Running a **TypeScript** config
262
+ directly? Launch the CLI under a TS loader:
263
+
264
+ ```bash
265
+ node --import tsx node_modules/@cronvello/sdk/dist/cli.js dev ./cronvello.config.ts
266
+ ```
267
+
268
+ **See what would fire without running anything:**
269
+
270
+ ```bash
271
+ npx cronvello dev --dry-run --window 1h # lists the fires due in the next hour
272
+ ```
273
+
274
+ **Preview any cron expression** — the next N fire times, in a timezone:
275
+
276
+ ```bash
277
+ npx cronvello preview "0 8 * * 1-5" --tz Europe/Berlin -n 5
278
+ npx cronvello preview "@daily" -n 3
279
+ ```
280
+
281
+ **Run a single job once** (handy in a script or while iterating on one handler):
282
+
283
+ ```bash
284
+ npx cronvello trigger daily-digest
285
+ ```
286
+
287
+ ### Local dashboard
288
+
289
+ Prefer to *see* your schedule? Add `--dashboard` and `cronvello dev` also serves a small local web UI
290
+ on top of the same engine — no account, no cloud, no external network.
291
+
292
+ ```bash
293
+ npx cronvello dev --dashboard # → http://127.0.0.1:4747 (override with --port)
294
+ ```
295
+
296
+ Open the printed URL and you get, live:
297
+
298
+ - every **job** with its cron expression, timezone, and a **countdown to the next fire**,
299
+ - a **run feed** that streams each fire, success, error, timeout, retry, and skip as it happens,
300
+ - a **job drawer** with recent runs and the next fire times, plus a **Run now** button that triggers
301
+ the handler through the engine and shows the result instantly,
302
+ - light + dark, and an honest **disconnected** state if the engine stops.
303
+
304
+ It binds to `127.0.0.1` by default (it's a dev tool, not a public server) and exposes a tiny
305
+ read-only JSON API plus an SSE stream — including `GET /api/runs.ndjson` to pipe the run history out
306
+ as NDJSON. You can also start it programmatically:
307
+
308
+ ```ts
309
+ const engine = cronvello.dev({ dashboard: true }); // or { dashboard: { port: 5000 } }
310
+ // … engine.stop() closes the dashboard too.
311
+ ```
312
+
313
+ ![Cronvello local dashboard](https://unpkg.com/@cronvello/sdk/docs/dashboard.png)
314
+
315
+ ### Embedding the engine
316
+
317
+ `cronvello dev` is a thin wrapper over `app.dev()`, which you can call yourself — it returns the
318
+ engine handle (run history, snapshot, clean `stop()`):
319
+
320
+ ```ts
321
+ const engine = cronvello.dev(); // starts the local scheduler
322
+ // … later …
323
+ console.log(engine.runs()); // recent run records, newest first
324
+ await engine.stop(); // drains in-flight runs, clears timers
325
+ ```
326
+
327
+ The lower-level primitives live under the **`@cronvello/sdk/dev`** subpath — including
328
+ `nextOccurrence`, `previewSchedule`, and `createLocalEngine` — so you can build the schedule math
329
+ into your own tooling:
330
+
331
+ ```ts
332
+ import { nextOccurrence, previewSchedule } from "@cronvello/sdk/dev";
333
+
334
+ nextOccurrence("0 8 * * *", { timeZone: "Europe/Berlin" }); // → next 08:00 in Berlin
335
+ previewSchedule("*/15 * * * *", { count: 4 }); // → the next four fire times
336
+ ```
337
+
338
+ > The local engine is fully MIT and **never makes a network call**. When your jobs are ready for
339
+ > production, `sync()` registers the exact same definitions with Cronvello Cloud — same code, now
340
+ > hosted, with reliability, alerts, and run history. Local is the on-ramp; the cloud is the upsell.
341
+
342
+ ---
343
+
210
344
  ## Lifecycle hooks & logging
211
345
 
212
346
  Observe every run — for structured logs, metrics, or error reporting (Sentry, etc.). Hooks never
@@ -331,12 +465,12 @@ npx cronvello secret # generate a strong dispatch secret
331
465
 
332
466
  ```
333
467
  $ cronvello whoami
334
- ╭─ System (internal siblings) ───────────────────╮
335
- │ account #1system@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 │
468
+ ╭─ Acme Production ──────────────────────────────╮
469
+ │ account #42ops@example.com
470
+ │ plan Pro
471
+ │ limits 100 jobs · 50 tasks/job · 600/min
472
+ │ usage 1842 / 10000 executions (4 jobs)
473
+ │ attention 0 DLQ · 0 heartbeats · 0 maintenance │
340
474
  ╰────────────────────────────────────────────────╯
341
475
  ```
342
476
 
@@ -346,7 +480,10 @@ $ cronvello whoami
346
480
  ```bash
347
481
  npx cronvello sync ./cronvello.config.js # reconcile to Cronvello
348
482
  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
483
+ npx cronvello dev ./cronvello.config.js # run the jobs locally (the local engine)
484
+ npx cronvello dev ./cronvello.config.js --dry-run # …or just show what would fire
485
+ npx cronvello preview "0 8 * * 1-5" --tz Europe/Berlin -n 5 # next fire times of an expression
486
+ npx cronvello trigger ./cronvello.config.js my-job # run one job's handler once, locally
350
487
  ```
351
488
 
352
489
  > Running a **TypeScript** config? Launch the CLI under a TS loader: