@cronvello/sdk 0.1.0 → 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/dist/cli.js ADDED
@@ -0,0 +1,874 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'module';
3
+ import { pathToFileURL } from 'url';
4
+ import { resolve } from 'path';
5
+ import { existsSync } from 'fs';
6
+
7
+ // src/internal/errors.ts
8
+ var CronvelloError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "CronvelloError";
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ };
15
+ var CronvelloApiError = class extends CronvelloError {
16
+ /** HTTP status code. */
17
+ status;
18
+ /** Machine-readable error code from the API body, when present. */
19
+ code;
20
+ /** The method + path that failed, e.g. `POST /v1/jobs`. */
21
+ endpoint;
22
+ /** Parsed response body (best effort). */
23
+ body;
24
+ /** Seconds to wait before retrying, parsed from `Retry-After` / rate-limit payload (429 only). */
25
+ retryAfterSeconds;
26
+ constructor(args) {
27
+ super(`[${args.status}] ${args.endpoint}: ${args.message}`);
28
+ this.name = "CronvelloApiError";
29
+ this.status = args.status;
30
+ this.code = args.code;
31
+ this.endpoint = args.endpoint;
32
+ this.body = args.body;
33
+ this.retryAfterSeconds = args.retryAfterSeconds;
34
+ Object.setPrototypeOf(this, new.target.prototype);
35
+ }
36
+ get isRateLimited() {
37
+ return this.status === 429;
38
+ }
39
+ get isAuthError() {
40
+ return this.status === 401 || this.status === 403;
41
+ }
42
+ get isNotFound() {
43
+ return this.status === 404;
44
+ }
45
+ };
46
+ var CronvelloNetworkError = class extends CronvelloError {
47
+ endpoint;
48
+ cause;
49
+ constructor(endpoint, cause) {
50
+ super(`Network error calling ${endpoint}: ${describe(cause)}`);
51
+ this.name = "CronvelloNetworkError";
52
+ this.endpoint = endpoint;
53
+ this.cause = cause;
54
+ Object.setPrototypeOf(this, new.target.prototype);
55
+ }
56
+ };
57
+ var CronvelloConfigError = class extends CronvelloError {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "CronvelloConfigError";
61
+ Object.setPrototypeOf(this, new.target.prototype);
62
+ }
63
+ };
64
+ function describe(cause) {
65
+ if (cause instanceof Error) return cause.message;
66
+ return String(cause);
67
+ }
68
+
69
+ // src/internal/http.ts
70
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
71
+ var Transport = class {
72
+ baseUrl;
73
+ apiKey;
74
+ timeoutMs;
75
+ maxRetries;
76
+ fetchImpl;
77
+ defaultHeaders;
78
+ onRequest;
79
+ constructor(opts) {
80
+ if (!opts.apiKey) throw new CronvelloConfigError("apiKey is required");
81
+ if (!opts.baseUrl) throw new CronvelloConfigError("baseUrl is required");
82
+ const resolvedFetch = opts.fetch ?? globalThis.fetch;
83
+ if (!resolvedFetch) {
84
+ throw new CronvelloConfigError(
85
+ "No global fetch found. Use Node 18+ or pass a `fetch` implementation in the client options."
86
+ );
87
+ }
88
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
89
+ this.apiKey = opts.apiKey;
90
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
91
+ this.maxRetries = opts.maxRetries ?? 2;
92
+ this.fetchImpl = resolvedFetch;
93
+ this.defaultHeaders = opts.defaultHeaders ?? {};
94
+ this.onRequest = opts.onRequest;
95
+ }
96
+ async request(opts) {
97
+ const url = this.buildUrl(opts.path, opts.query);
98
+ const endpoint = `${opts.method} ${opts.path}`;
99
+ const headers = {
100
+ authorization: `Bearer ${this.apiKey}`,
101
+ accept: "application/json",
102
+ ...this.defaultHeaders
103
+ };
104
+ let payload;
105
+ if (opts.body !== void 0) {
106
+ headers["content-type"] = "application/json";
107
+ payload = JSON.stringify(opts.body);
108
+ }
109
+ if (opts.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
110
+ let lastError;
111
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
112
+ const startedAt = Date.now();
113
+ const { signal, cancel } = this.withTimeout(opts.signal);
114
+ try {
115
+ const res = await this.fetchImpl(url, {
116
+ method: opts.method,
117
+ headers,
118
+ ...payload !== void 0 ? { body: payload } : {},
119
+ signal
120
+ });
121
+ cancel();
122
+ const durationMs = Date.now() - startedAt;
123
+ this.onRequest?.({ method: opts.method, path: opts.path, status: res.status, attempt, durationMs });
124
+ const text = await res.text();
125
+ const parsed = text ? safeJson(text) : null;
126
+ if (res.ok) return unwrapEnvelope(parsed);
127
+ const retryAfter = parseRetryAfter(res, parsed);
128
+ if (RETRYABLE_STATUS.has(res.status) && attempt < this.maxRetries) {
129
+ await sleep(this.backoff(attempt, retryAfter));
130
+ continue;
131
+ }
132
+ throw new CronvelloApiError({
133
+ status: res.status,
134
+ endpoint,
135
+ message: extractMessage(parsed) ?? `Request failed`,
136
+ code: extractCode(parsed),
137
+ body: parsed,
138
+ ...retryAfter !== void 0 ? { retryAfterSeconds: retryAfter } : {}
139
+ });
140
+ } catch (err) {
141
+ cancel();
142
+ if (err instanceof CronvelloApiError) throw err;
143
+ lastError = err;
144
+ if (attempt < this.maxRetries) {
145
+ await sleep(this.backoff(attempt));
146
+ continue;
147
+ }
148
+ throw new CronvelloNetworkError(endpoint, err);
149
+ }
150
+ }
151
+ throw new CronvelloNetworkError(endpoint, lastError);
152
+ }
153
+ buildUrl(path, query) {
154
+ const base = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
155
+ if (!query) return base;
156
+ const params = Object.entries(query).filter(([, v]) => v !== void 0).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
157
+ return params.length ? `${base}?${params.join("&")}` : base;
158
+ }
159
+ withTimeout(external) {
160
+ const controller = new AbortController();
161
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
162
+ const onExternalAbort = () => controller.abort();
163
+ if (external) {
164
+ if (external.aborted) controller.abort();
165
+ else external.addEventListener("abort", onExternalAbort, { once: true });
166
+ }
167
+ return {
168
+ signal: controller.signal,
169
+ cancel: () => {
170
+ clearTimeout(timer);
171
+ external?.removeEventListener("abort", onExternalAbort);
172
+ }
173
+ };
174
+ }
175
+ backoff(attempt, retryAfterSeconds) {
176
+ if (retryAfterSeconds !== void 0) return Math.min(retryAfterSeconds * 1e3, 3e4);
177
+ const base = Math.min(300 * 2 ** attempt, 5e3);
178
+ return Math.floor(Math.random() * base);
179
+ }
180
+ };
181
+ function sleep(ms) {
182
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
183
+ }
184
+ function safeJson(text) {
185
+ try {
186
+ return JSON.parse(text);
187
+ } catch {
188
+ return text;
189
+ }
190
+ }
191
+ function unwrapEnvelope(body) {
192
+ if (body !== null && typeof body === "object" && "success" in body && typeof body.success === "boolean" && "data" in body) {
193
+ return body.data;
194
+ }
195
+ return body;
196
+ }
197
+ function extractMessage(body) {
198
+ if (body && typeof body === "object" && "message" in body) {
199
+ const m = body.message;
200
+ if (typeof m === "string") return m;
201
+ }
202
+ return void 0;
203
+ }
204
+ function extractCode(body) {
205
+ if (body && typeof body === "object" && "code" in body) {
206
+ const c2 = body.code;
207
+ if (typeof c2 === "string") return c2;
208
+ }
209
+ return void 0;
210
+ }
211
+ function parseRetryAfter(res, body) {
212
+ const header2 = res.headers.get("retry-after");
213
+ if (header2) {
214
+ const n = Number(header2);
215
+ if (Number.isFinite(n)) return n;
216
+ }
217
+ if (body && typeof body === "object" && "data" in body) {
218
+ const data = body.data;
219
+ if (data && typeof data.retryAfterSeconds === "number") return data.retryAfterSeconds;
220
+ }
221
+ return void 0;
222
+ }
223
+
224
+ // src/client/client.ts
225
+ var CRONVELLO_DEFAULT_BASE_URL = "https://api.cronvello.com";
226
+ var CronvelloClient = class {
227
+ transport;
228
+ /** The resolved base URL in use. */
229
+ baseUrl;
230
+ /** Job container operations. */
231
+ jobs;
232
+ /** Scheduled task operations. */
233
+ tasks;
234
+ /** Execution-history (run) operations. */
235
+ runs;
236
+ /** Account identity + usage. */
237
+ account;
238
+ constructor(options) {
239
+ if (!options || !options.apiKey) {
240
+ throw new CronvelloConfigError("CronvelloClient requires an `apiKey` (crn_live_\u2026).");
241
+ }
242
+ this.baseUrl = (options.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL).replace(/\/+$/, "");
243
+ this.transport = new Transport({
244
+ baseUrl: this.baseUrl,
245
+ apiKey: options.apiKey,
246
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
247
+ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
248
+ ...options.fetch ? { fetch: options.fetch } : {},
249
+ ...options.onRequest ? { onRequest: options.onRequest } : {},
250
+ defaultHeaders: { "user-agent": "cronvello-sdk" }
251
+ });
252
+ this.jobs = new JobsResource(this.transport);
253
+ this.tasks = new TasksResource(this.transport);
254
+ this.runs = new RunsResource(this.transport);
255
+ this.account = new AccountResource(this.transport);
256
+ }
257
+ /**
258
+ * Atomically reconcile a whole code registry server-side (`PUT /v1/registry`): one job
259
+ * container + its tasks, created/updated/pruned/started in a single call. This is what
260
+ * `defineCronvello().sync()` prefers; the high-level API builds the body for you.
261
+ */
262
+ reconcileRegistry(body, opts) {
263
+ return this.transport.request({
264
+ method: "PUT",
265
+ path: "/v1/registry",
266
+ body,
267
+ ...opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
268
+ });
269
+ }
270
+ /**
271
+ * Escape hatch for endpoints not yet wrapped by a typed method (heartbeat monitors,
272
+ * maintenance windows, DLQ, notification channels, audit log, API-key self-service …).
273
+ * `T` is the response shape you expect.
274
+ */
275
+ request(method, path, opts) {
276
+ return this.transport.request({
277
+ method,
278
+ path,
279
+ ...opts?.query ? { query: opts.query } : {},
280
+ ...opts?.body !== void 0 ? { body: opts.body } : {},
281
+ ...opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
282
+ });
283
+ }
284
+ };
285
+ var JobsResource = class {
286
+ constructor(t) {
287
+ this.t = t;
288
+ }
289
+ t;
290
+ list() {
291
+ return this.t.request({ method: "GET", path: "/v1/jobs" });
292
+ }
293
+ get(jobId) {
294
+ return this.t.request({ method: "GET", path: `/v1/jobs/${enc(jobId)}` });
295
+ }
296
+ create(body, opts) {
297
+ return this.t.request({ method: "POST", path: "/v1/jobs", body, ...opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {} });
298
+ }
299
+ update(jobId, body) {
300
+ return this.t.request({ method: "PATCH", path: `/v1/jobs/${enc(jobId)}`, body });
301
+ }
302
+ delete(jobId) {
303
+ return this.t.request({ method: "DELETE", path: `/v1/jobs/${enc(jobId)}` });
304
+ }
305
+ start(jobId) {
306
+ return this.t.request({ method: "POST", path: `/v1/jobs/${enc(jobId)}/start` });
307
+ }
308
+ stop(jobId) {
309
+ return this.t.request({ method: "POST", path: `/v1/jobs/${enc(jobId)}/stop` });
310
+ }
311
+ listTasks(jobId) {
312
+ return this.t.request({ method: "GET", path: `/v1/jobs/${enc(jobId)}/tasks` });
313
+ }
314
+ createTask(jobId, body, opts) {
315
+ return this.t.request({ method: "POST", path: `/v1/jobs/${enc(jobId)}/tasks`, body, ...opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {} });
316
+ }
317
+ };
318
+ var TasksResource = class {
319
+ constructor(t) {
320
+ this.t = t;
321
+ }
322
+ t;
323
+ /** Account-wide paginated task table. */
324
+ list(query) {
325
+ return this.t.request({ method: "GET", path: "/v1/tasks", ...query ? { query } : {} });
326
+ }
327
+ get(taskId) {
328
+ return this.t.request({ method: "GET", path: `/v1/tasks/${enc(taskId)}` });
329
+ }
330
+ update(taskId, body) {
331
+ return this.t.request({ method: "PATCH", path: `/v1/tasks/${enc(taskId)}`, body });
332
+ }
333
+ delete(taskId) {
334
+ return this.t.request({ method: "DELETE", path: `/v1/tasks/${enc(taskId)}` });
335
+ }
336
+ start(taskId) {
337
+ return this.t.request({ method: "POST", path: `/v1/tasks/${enc(taskId)}/start` });
338
+ }
339
+ stop(taskId) {
340
+ return this.t.request({ method: "POST", path: `/v1/tasks/${enc(taskId)}/stop` });
341
+ }
342
+ /** Trigger a one-off manual execution (does not change the schedule). */
343
+ runNow(taskId) {
344
+ return this.t.request({ method: "POST", path: `/v1/tasks/${enc(taskId)}/run` });
345
+ }
346
+ listRuns(taskId, query) {
347
+ return this.t.request({ method: "GET", path: `/v1/tasks/${enc(taskId)}/runs`, ...query ? { query } : {} });
348
+ }
349
+ };
350
+ var RunsResource = class {
351
+ constructor(t) {
352
+ this.t = t;
353
+ }
354
+ t;
355
+ /** Account-wide paginated activity feed. */
356
+ list(query) {
357
+ return this.t.request({ method: "GET", path: "/v1/runs", ...query ? { query } : {} });
358
+ }
359
+ get(runId) {
360
+ return this.t.request({ method: "GET", path: `/v1/runs/${enc(runId)}` });
361
+ }
362
+ };
363
+ var AccountResource = class {
364
+ constructor(t) {
365
+ this.t = t;
366
+ }
367
+ t;
368
+ me() {
369
+ return this.t.request({ method: "GET", path: "/v1/me" });
370
+ }
371
+ usage() {
372
+ return this.t.request({ method: "GET", path: "/v1/usage" });
373
+ }
374
+ };
375
+ function enc(segment) {
376
+ return encodeURIComponent(segment);
377
+ }
378
+
379
+ // src/registry/format.ts
380
+ var ESC = String.fromCharCode(27);
381
+ var ANSI = {
382
+ reset: `${ESC}[0m`,
383
+ dim: `${ESC}[2m`,
384
+ bold: `${ESC}[1m`,
385
+ green: `${ESC}[32m`,
386
+ yellow: `${ESC}[33m`,
387
+ red: `${ESC}[31m`,
388
+ cyan: `${ESC}[36m`,
389
+ gray: `${ESC}[90m`
390
+ };
391
+ var GLYPH = {
392
+ created: { sign: "+", color: "green" },
393
+ updated: { sign: "~", color: "yellow" },
394
+ unchanged: { sign: "=", color: "gray" },
395
+ deleted: { sign: "-", color: "red" },
396
+ skipped: { sign: "\xB7", color: "cyan" }
397
+ };
398
+ function formatSyncResult(result, options = {}) {
399
+ const paint = (s, c2) => options.color ? `${ANSI[c2]}${s}${ANSI.reset}` : s;
400
+ const verb = options.dryRun ? "would sync" : "synced";
401
+ const lines = [];
402
+ lines.push(
403
+ `${paint("Cronvello", "cyan")} ${verb} ${paint(`"${result.jobName}"`, "bold")} ${paint(`(${result.jobId})`, "gray")}`
404
+ );
405
+ const order = ["created", "updated", "deleted", "skipped", "unchanged"];
406
+ const sorted = [...result.changes].sort((a, b) => order.indexOf(a.action) - order.indexOf(b.action));
407
+ for (const c2 of sorted) {
408
+ const g = GLYPH[c2.action];
409
+ const detail = c2.changedFields && c2.changedFields.length ? paint(` (${c2.changedFields.join(", ")})`, "gray") : c2.reason ? paint(` (${c2.reason})`, "gray") : "";
410
+ lines.push(` ${paint(g.sign, g.color)} ${paint(c2.action.padEnd(9), g.color)} ${c2.key}${detail}`);
411
+ }
412
+ const tally = [
413
+ result.created && `${result.created} created`,
414
+ result.updated && `${result.updated} updated`,
415
+ result.unchanged && `${result.unchanged} unchanged`,
416
+ result.deleted && `${result.deleted} deleted`,
417
+ result.skipped && `${result.skipped} skipped`
418
+ ].filter(Boolean);
419
+ const summary = tally.length ? tally.join(", ") : "no changes";
420
+ const containerNote = result.jobCreated ? paint(" \xB7 container created", "gray") : "";
421
+ lines.push(` ${paint(summary, "bold")}${containerNote}`);
422
+ return lines.join("\n");
423
+ }
424
+
425
+ // src/index.ts
426
+ function generateDispatchSecret(bytes = 32) {
427
+ const buf = new Uint8Array(bytes);
428
+ crypto.getRandomValues(buf);
429
+ return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
430
+ }
431
+
432
+ // src/cli/ui.ts
433
+ var E = String.fromCharCode(27);
434
+ var ENV = typeof process !== "undefined" && process.env ? process.env : {};
435
+ var COLOR = !ENV["NO_COLOR"] && (!!ENV["FORCE_COLOR"] || !!(typeof process !== "undefined" && process.stdout && process.stdout.isTTY));
436
+ function setColor(on) {
437
+ COLOR = on;
438
+ }
439
+ function wrap(code, close, s) {
440
+ return COLOR ? `${E}[${code}m${s}${E}[${close}m` : s;
441
+ }
442
+ var c = {
443
+ bold: (s) => wrap(1, 22, s),
444
+ dim: (s) => wrap(2, 22, s),
445
+ red: (s) => wrap(31, 39, s),
446
+ green: (s) => wrap(32, 39, s),
447
+ yellow: (s) => wrap(33, 39, s),
448
+ blue: (s) => wrap(34, 39, s),
449
+ magenta: (s) => wrap(35, 39, s),
450
+ cyan: (s) => wrap(36, 39, s),
451
+ gray: (s) => wrap(90, 39, s)
452
+ };
453
+ function brand(s) {
454
+ return COLOR ? `${E}[38;5;63m${s}${E}[39m` : s;
455
+ }
456
+ var sym = {
457
+ ok: c.green("\u2714"),
458
+ fail: c.red("\u2717"),
459
+ warn: c.yellow("\u25B2"),
460
+ info: c.cyan("\u2139"),
461
+ dot: c.gray("\xB7"),
462
+ arrow: c.gray("\u2192")
463
+ };
464
+ function visibleLength(s) {
465
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
466
+ }
467
+ function padEndVisible(s, width) {
468
+ const pad = width - visibleLength(s);
469
+ return pad > 0 ? s + " ".repeat(pad) : s;
470
+ }
471
+ function table(columns, rows) {
472
+ const widths = columns.map(
473
+ (col, i) => Math.max(visibleLength(col.header), ...rows.map((r) => visibleLength(r[i] ?? "")))
474
+ );
475
+ const fmtRow = (cells) => cells.map((cell, i) => {
476
+ const w = widths[i];
477
+ if (columns[i]?.align === "right") {
478
+ const pad = w - visibleLength(cell);
479
+ return (pad > 0 ? " ".repeat(pad) : "") + cell;
480
+ }
481
+ return padEndVisible(cell, w);
482
+ }).join(" ");
483
+ const head = fmtRow(columns.map((col) => c.dim(col.header)));
484
+ const body = rows.map(fmtRow);
485
+ return [head, ...body].join("\n");
486
+ }
487
+ function box(title, lines) {
488
+ const inner = [title, "", ...lines];
489
+ const width = Math.max(...inner.map(visibleLength));
490
+ const top = c.gray("\u256D\u2500 ") + brand(title) + c.gray(" " + "\u2500".repeat(Math.max(0, width - visibleLength(title) - 1)) + "\u256E");
491
+ const mid = lines.map((l) => c.gray("\u2502 ") + padEndVisible(l, width) + c.gray(" \u2502"));
492
+ const bot = c.gray("\u2570" + "\u2500".repeat(width + 2) + "\u256F");
493
+ return [top, ...mid, bot].join("\n");
494
+ }
495
+ function relativeTime(iso, nowMs) {
496
+ if (!iso) return c.gray("\u2014");
497
+ const then = Date.parse(iso);
498
+ if (Number.isNaN(then)) return c.gray("\u2014");
499
+ const deltaSec = Math.round((then - nowMs) / 1e3);
500
+ const future = deltaSec > 0;
501
+ const abs = Math.abs(deltaSec);
502
+ const unit = abs < 60 ? `${abs}s` : abs < 3600 ? `${Math.round(abs / 60)}m` : abs < 86400 ? `${Math.round(abs / 3600)}h` : `${Math.round(abs / 86400)}d`;
503
+ return c.gray(future ? `in ${unit}` : `${unit} ago`);
504
+ }
505
+ var ICON = brand("\u25F7");
506
+
507
+ // src/cli/index.ts
508
+ var require2 = createRequire(import.meta.url);
509
+ var VERSION = (() => {
510
+ try {
511
+ return require2("../package.json").version;
512
+ } catch {
513
+ return "0.0.0";
514
+ }
515
+ })();
516
+ function parseArgs(argv) {
517
+ const positionals = [];
518
+ const flags = {};
519
+ for (let i = 0; i < argv.length; i++) {
520
+ const a = argv[i];
521
+ if (a.startsWith("--")) {
522
+ const key = a.slice(2);
523
+ const next = argv[i + 1];
524
+ if (next !== void 0 && !next.startsWith("-") && ["limit", "status", "job", "type", "runType"].includes(key)) {
525
+ flags[key] = next;
526
+ i++;
527
+ } else {
528
+ flags[key] = true;
529
+ }
530
+ } else if (a.startsWith("-") && a.length > 1) {
531
+ flags[a.slice(1)] = true;
532
+ } else {
533
+ positionals.push(a);
534
+ }
535
+ }
536
+ return { command: positionals[0] ?? "", positionals: positionals.slice(1), flags };
537
+ }
538
+ var out = (s = "") => void process.stdout.write(s + "\n");
539
+ var json = (v) => out(JSON.stringify(v, null, 2));
540
+ function makeClient() {
541
+ const apiKey = process.env["CRONVELLO_API_KEY"];
542
+ if (!apiKey) {
543
+ throw new CronvelloConfigError("CRONVELLO_API_KEY is not set. Export your crn_live_\u2026 key first.");
544
+ }
545
+ const baseUrl = process.env["CRONVELLO_API_URL"];
546
+ return new CronvelloClient({ apiKey, ...baseUrl ? { baseUrl } : {} });
547
+ }
548
+ function statusColor(status) {
549
+ const s = status.toUpperCase();
550
+ if (s === "ACTIVE" || s === "COMPLETED") return c.green(status);
551
+ if (s === "DISABLED" || s === "QUEUED" || s === "RUNNING") return c.cyan(status);
552
+ if (s.startsWith("ERROR") || s === "FAILED") return c.red(status);
553
+ return c.yellow(status);
554
+ }
555
+ function header(subtitle) {
556
+ out(`${ICON} ${brand("Cronvello")} ${c.gray(subtitle)}`);
557
+ out();
558
+ }
559
+ async function cmdWhoami(useJson) {
560
+ const me = await makeClient().account.me();
561
+ if (useJson) return json(me);
562
+ const u = me.usage;
563
+ const quota = u.executionQuota == null ? "\u221E" : String(u.executionQuota);
564
+ out(
565
+ box(`${me.accountName}`, [
566
+ `${c.dim("account")} #${me.accountId} ${me.isActive ? sym.ok : sym.fail} ${me.email}`,
567
+ `${c.dim("plan")} ${brand(me.plan?.displayName ?? "\u2014")}`,
568
+ `${c.dim("limits")} ${me.limits.maxJobs ?? "\u221E"} jobs \xB7 ${me.limits.maxTasksPerJob ?? "\u221E"} tasks/job \xB7 ${me.limits.rateLimitPerMinute ?? "\u221E"}/min`,
569
+ `${c.dim("usage")} ${c.bold(String(u.executionCount))} / ${quota} executions ${c.gray(`(${u.jobsUsed} jobs)`)}`,
570
+ `${c.dim("attention")} ${me.summary.openDlqCount} DLQ \xB7 ${me.summary.heartbeatNeedsAttention} heartbeats \xB7 ${me.summary.activeMaintenanceWindows} maintenance`
571
+ ])
572
+ );
573
+ }
574
+ async function cmdList(jobName, useJson) {
575
+ const client = makeClient();
576
+ if (jobName) {
577
+ const container = (await client.jobs.list()).find((j) => j.name === jobName);
578
+ if (!container) throw new CronvelloConfigError(`No job container named "${jobName}".`);
579
+ const tasks = await client.jobs.listTasks(container.id);
580
+ if (useJson) return json(tasks);
581
+ header(`tasks in "${container.name}"`);
582
+ out(renderTasks(tasks));
583
+ return;
584
+ }
585
+ const jobs = await client.jobs.list();
586
+ if (useJson) return json(jobs);
587
+ header("job containers");
588
+ out(
589
+ table(
590
+ [{ header: "NAME" }, { header: "STATUS" }, { header: "TASKS", align: "right" }, { header: "ACTIVE", align: "right" }, { header: "ERRORS", align: "right" }, { header: "ID" }],
591
+ jobs.map((j) => [
592
+ c.bold(j.name),
593
+ statusColor(j.status),
594
+ String(j.taskCount),
595
+ String(j.activeTaskCount),
596
+ j.errorTaskCount ? c.red(String(j.errorTaskCount)) : c.gray("0"),
597
+ c.gray(j.id)
598
+ ])
599
+ )
600
+ );
601
+ out();
602
+ out(c.gray(`${jobs.length} container(s) \xB7 ${jobs.reduce((n, j) => n + j.taskCount, 0)} task(s)`));
603
+ }
604
+ function renderTasks(tasks) {
605
+ const now = Date.now();
606
+ return table(
607
+ [{ header: "TASK" }, { header: "STATUS" }, { header: "SCHEDULE" }, { header: "NEXT RUN" }, { header: "LAST" }],
608
+ tasks.map((t) => [
609
+ c.bold(t.name),
610
+ statusColor(t.status),
611
+ c.cyan(t.schedule),
612
+ relativeTime(t.nextRun, now),
613
+ t.lastRunStatus ? statusColor(t.lastRunStatus) : c.gray("\u2014")
614
+ ])
615
+ );
616
+ }
617
+ async function cmdTasks(flags, useJson) {
618
+ const client = makeClient();
619
+ const jobName = typeof flags["job"] === "string" ? flags["job"] : void 0;
620
+ let jobId;
621
+ if (jobName) {
622
+ const container = (await client.jobs.list()).find((j) => j.name === jobName);
623
+ if (!container) throw new CronvelloConfigError(`No job container named "${jobName}".`);
624
+ jobId = container.id;
625
+ }
626
+ const page = await client.tasks.list({ limit: 100, ...jobId ? { jobId } : {} });
627
+ if (useJson) return json(page);
628
+ header(jobName ? `tasks \xB7 ${jobName}` : "all tasks");
629
+ out(renderTasks(page.tasks));
630
+ }
631
+ async function cmdRuns(flags, useJson) {
632
+ const client = makeClient();
633
+ const limit = clampLimit(flags["limit"]);
634
+ const status = typeof flags["status"] === "string" ? flags["status"] : void 0;
635
+ const page = await client.runs.list({ limit, ...status ? { status } : {} });
636
+ if (useJson) return json(page);
637
+ header(`recent runs${status ? ` \xB7 ${status}` : ""}`);
638
+ const now = Date.now();
639
+ out(
640
+ table(
641
+ [{ header: "WHEN" }, { header: "TASK" }, { header: "JOB" }, { header: "STATUS" }, { header: "HTTP", align: "right" }, { header: "TOOK", align: "right" }],
642
+ page.runs.map((r) => [
643
+ relativeTime(r.startedAt, now),
644
+ c.bold(r.taskName),
645
+ c.gray(r.jobName),
646
+ statusColor(r.status),
647
+ r.httpStatusCode == null ? c.gray("\u2014") : String(r.httpStatusCode),
648
+ r.durationMs == null ? c.gray("\u2014") : `${r.durationMs}ms`
649
+ ])
650
+ )
651
+ );
652
+ out();
653
+ out(c.gray(`showing ${page.runs.length} of ${page.pagination.total} runs`));
654
+ }
655
+ async function cmdRun(positionals, useJson) {
656
+ const client = makeClient();
657
+ let taskId;
658
+ if (positionals.length >= 2) {
659
+ const [jobName, taskName] = positionals;
660
+ const container = (await client.jobs.list()).find((j) => j.name === jobName);
661
+ if (!container) throw new CronvelloConfigError(`No job container named "${jobName}".`);
662
+ const task = (await client.jobs.listTasks(container.id)).find((t) => t.name === taskName);
663
+ if (!task) throw new CronvelloConfigError(`No task "${taskName}" in "${jobName}".`);
664
+ taskId = task.id;
665
+ } else if (positionals.length === 1) {
666
+ taskId = positionals[0];
667
+ } else {
668
+ throw new CronvelloConfigError("Usage: cronvello run <taskId> | cronvello run <jobName> <taskName>");
669
+ }
670
+ const res = await client.tasks.runNow(taskId);
671
+ if (useJson) return json(res);
672
+ if (res.success) out(`${sym.ok} triggered ${c.bold(taskId)} ${res.runId ? c.gray(`\u2192 ${res.runId}`) : ""}`);
673
+ else out(`${sym.fail} ${c.red(res.error ?? "run failed")}`);
674
+ }
675
+ async function cmdStatus(useJson) {
676
+ const client = makeClient();
677
+ const [me, jobs, failures] = await Promise.all([
678
+ client.account.me(),
679
+ client.jobs.list(),
680
+ client.runs.list({ limit: 5, status: "failed" })
681
+ ]);
682
+ if (useJson) return json({ me, jobs, recentFailures: failures.runs });
683
+ const totalTasks = jobs.reduce((n, j) => n + j.taskCount, 0);
684
+ const activeTasks = jobs.reduce((n, j) => n + j.activeTaskCount, 0);
685
+ const errorTasks = jobs.reduce((n, j) => n + j.errorTaskCount, 0);
686
+ const healthy = errorTasks === 0 && me.summary.openDlqCount === 0;
687
+ header("status");
688
+ out(`${healthy ? sym.ok : sym.warn} ${healthy ? c.green("healthy") : c.yellow("needs attention")}`);
689
+ out();
690
+ out(` ${c.dim("containers")} ${jobs.length}`);
691
+ out(` ${c.dim("tasks")} ${activeTasks}/${totalTasks} active${errorTasks ? c.red(` \xB7 ${errorTasks} errored`) : ""}`);
692
+ out(` ${c.dim("open DLQ")} ${me.summary.openDlqCount ? c.red(String(me.summary.openDlqCount)) : c.gray("0")}`);
693
+ out(` ${c.dim("executions")} ${me.usage.executionCount} this period`);
694
+ if (failures.runs.length) {
695
+ out();
696
+ out(c.dim(" recent failures:"));
697
+ const now = Date.now();
698
+ for (const r of failures.runs) out(` ${sym.fail} ${c.bold(r.taskName)} ${c.gray(r.jobName)} ${relativeTime(r.startedAt, now)} ${r.error ? c.red(truncate(r.error, 50)) : ""}`);
699
+ }
700
+ }
701
+ async function cmdSync(positionals, flags, useJson) {
702
+ const app = await loadApp(positionals[0]);
703
+ const dryRun = !!flags["dry"] || !!flags["dry-run"];
704
+ const result = await app.sync({ dryRun });
705
+ if (useJson) return json(result);
706
+ out(formatSyncResult(result, { color: true, dryRun }));
707
+ }
708
+ async function cmdDev(positionals, useJson) {
709
+ const jobKey = positionals[positionals.length - 1];
710
+ const configPath = positionals.length >= 2 ? positionals[0] : void 0;
711
+ if (!jobKey) throw new CronvelloConfigError("Usage: cronvello dev [configPath] <jobKey>");
712
+ const app = await loadApp(configPath);
713
+ if (!app.keys().includes(jobKey)) {
714
+ throw new CronvelloConfigError(`Unknown job "${jobKey}". Known: ${app.keys().join(", ") || "(none)"}`);
715
+ }
716
+ if (!useJson) out(`${sym.arrow} running ${c.bold(jobKey)} locally\u2026`);
717
+ const started = Date.now();
718
+ const result = await app.trigger(jobKey);
719
+ if (useJson) return json({ job: jobKey, result });
720
+ out(`${sym.ok} ${c.bold(jobKey)} ${c.gray(`done in ${Date.now() - started}ms`)}`);
721
+ if (result !== void 0 && result !== null) {
722
+ out(c.dim(" result:"));
723
+ out(JSON.stringify(result, null, 2).split("\n").map((l) => " " + l).join("\n"));
724
+ }
725
+ }
726
+ function cmdSecret(useJson) {
727
+ const secret = generateDispatchSecret();
728
+ if (useJson) return json({ dispatchSecret: secret });
729
+ out(secret);
730
+ }
731
+ var DEFAULT_CONFIG_PATHS = [
732
+ "cronvello.config.ts",
733
+ "cronvello.config.js",
734
+ "cronvello.config.mjs",
735
+ "cronvello.ts",
736
+ "cronvello.js",
737
+ "src/cronvello.ts",
738
+ "src/cronvello.js"
739
+ ];
740
+ function looksLikeApp(v) {
741
+ return !!v && typeof v === "object" && typeof v.sync === "function" && typeof v.trigger === "function";
742
+ }
743
+ async function loadApp(pathArg) {
744
+ const candidate = pathArg ?? DEFAULT_CONFIG_PATHS.find((p) => existsSync(resolve(process.cwd(), p)));
745
+ if (!candidate) {
746
+ throw new CronvelloConfigError(
747
+ `No config module found. Pass a path (cronvello sync ./cronvello.config.js) or add one of: ${DEFAULT_CONFIG_PATHS.join(", ")}.`
748
+ );
749
+ }
750
+ const abs = resolve(process.cwd(), candidate);
751
+ if (!existsSync(abs)) throw new CronvelloConfigError(`Config module not found: ${abs}`);
752
+ let mod;
753
+ try {
754
+ mod = await import(pathToFileURL(abs).href);
755
+ } catch (err) {
756
+ const msg = err instanceof Error ? err.message : String(err);
757
+ if (/Unknown file extension|\.ts|ERR_UNKNOWN/.test(msg) && abs.endsWith(".ts")) {
758
+ throw new CronvelloConfigError(
759
+ `Could not import a TypeScript config directly. Run the CLI under a TS loader, e.g.
760
+ node --import tsx node_modules/@cronvello/sdk/dist/cli.js sync ${candidate}
761
+ or point at a compiled .js file.`
762
+ );
763
+ }
764
+ throw new CronvelloConfigError(`Failed to import ${candidate}: ${msg}`);
765
+ }
766
+ const found = looksLikeApp(mod["default"]) ? mod["default"] : looksLikeApp(mod["cronvello"]) ? mod["cronvello"] : looksLikeApp(mod["app"]) ? mod["app"] : Object.values(mod).find(looksLikeApp);
767
+ if (!found) {
768
+ throw new CronvelloConfigError(`${candidate} does not export a Cronvello app (export default defineCronvello({\u2026}) or a named \`cronvello\`).`);
769
+ }
770
+ return found;
771
+ }
772
+ function printHelp() {
773
+ out(`${ICON} ${brand("Cronvello")} ${c.gray("\xB7 code-first cron")} ${c.dim("v" + VERSION)}`);
774
+ out();
775
+ out(c.bold("USAGE"));
776
+ out(` ${c.cyan("cronvello")} <command> [options]`);
777
+ out();
778
+ out(c.bold("COMMANDS"));
779
+ const cmds = [
780
+ ["whoami", "account, plan, limits & usage"],
781
+ ["list [jobName]", "job containers, or tasks inside one"],
782
+ ["tasks [--job <name>]", "account-wide task table"],
783
+ ["runs [--limit n] [--status s]", "recent execution feed"],
784
+ ["run <taskId>", "trigger a task now (or: run <jobName> <taskName>)"],
785
+ ["status", "health overview + recent failures"],
786
+ ["sync [path] [--dry]", "reconcile a code registry (loads your module)"],
787
+ ["dev [path] <jobKey>", "run a job handler locally, no deploy"],
788
+ ["secret", "generate a strong dispatch secret"]
789
+ ];
790
+ for (const [name, desc] of cmds) out(` ${c.cyan(name.padEnd(30))} ${c.gray(desc)}`);
791
+ out();
792
+ out(c.bold("GLOBAL"));
793
+ out(` ${c.cyan("--json".padEnd(30))} ${c.gray("machine-readable output")}`);
794
+ out(` ${c.cyan("--no-color".padEnd(30))} ${c.gray("disable colour")}`);
795
+ out(` ${c.cyan("-h, --help".padEnd(30))} ${c.gray("show this help")}`);
796
+ out(` ${c.cyan("-v, --version".padEnd(30))} ${c.gray("print version")}`);
797
+ out();
798
+ out(c.gray("Auth: export CRONVELLO_API_KEY (crn_live_\u2026). Optional: CRONVELLO_API_URL."));
799
+ }
800
+ function clampLimit(v) {
801
+ const n = typeof v === "string" ? Number(v) : NaN;
802
+ if (!Number.isFinite(n)) return 20;
803
+ return Math.min(Math.max(Math.trunc(n), 1), 100);
804
+ }
805
+ function truncate(s, n) {
806
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
807
+ }
808
+ async function run(argv) {
809
+ const args = parseArgs(argv);
810
+ if (args.flags["no-color"]) setColor(false);
811
+ const useJson = !!args.flags["json"];
812
+ if (args.flags["version"] || args.flags["v"]) {
813
+ out(VERSION);
814
+ return 0;
815
+ }
816
+ if (!args.command || args.flags["help"] || args.flags["h"] || args.command === "help") {
817
+ printHelp();
818
+ return 0;
819
+ }
820
+ try {
821
+ switch (args.command) {
822
+ case "whoami":
823
+ await cmdWhoami(useJson);
824
+ break;
825
+ case "list":
826
+ await cmdList(args.positionals[0], useJson);
827
+ break;
828
+ case "tasks":
829
+ await cmdTasks(args.flags, useJson);
830
+ break;
831
+ case "runs":
832
+ await cmdRuns(args.flags, useJson);
833
+ break;
834
+ case "run":
835
+ await cmdRun(args.positionals, useJson);
836
+ break;
837
+ case "status":
838
+ await cmdStatus(useJson);
839
+ break;
840
+ case "sync":
841
+ await cmdSync(args.positionals, args.flags, useJson);
842
+ break;
843
+ case "dev":
844
+ await cmdDev(args.positionals, useJson);
845
+ break;
846
+ case "secret":
847
+ cmdSecret(useJson);
848
+ break;
849
+ default:
850
+ out(`${sym.fail} Unknown command "${args.command}". Run ${c.cyan("cronvello --help")}.`);
851
+ return 1;
852
+ }
853
+ return 0;
854
+ } catch (err) {
855
+ if (err instanceof CronvelloApiError) {
856
+ out(`${sym.fail} ${c.red(`API ${err.status}`)} ${err.message}${err.isRateLimited && err.retryAfterSeconds ? c.gray(` (retry in ${err.retryAfterSeconds}s)`) : ""}`);
857
+ } else if (err instanceof CronvelloConfigError) {
858
+ out(`${sym.fail} ${err.message}`);
859
+ } else {
860
+ out(`${sym.fail} ${err instanceof Error ? err.message : String(err)}`);
861
+ }
862
+ return 1;
863
+ }
864
+ }
865
+ var invokedDirectly = typeof process !== "undefined" && Array.isArray(process.argv) && /[\\/]cli\.(c?js|ts)$/.test(process.argv[1] ?? "");
866
+ if (invokedDirectly) {
867
+ run(process.argv.slice(2)).then((code) => {
868
+ process.exitCode = code;
869
+ });
870
+ }
871
+
872
+ export { run };
873
+ //# sourceMappingURL=cli.js.map
874
+ //# sourceMappingURL=cli.js.map