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