@workoutx/sdk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,573 @@
1
+ // src/errors.ts
2
+ var WorkoutXError = class extends Error {
3
+ constructor(args) {
4
+ super(args.message);
5
+ this.name = "WorkoutXError";
6
+ this.status = args.status;
7
+ this.path = args.path;
8
+ this.body = args.body;
9
+ this.code = args.code;
10
+ this.tip = args.tip;
11
+ this.docs = args.docs;
12
+ }
13
+ /** True for 429 (rate limit / quota). */
14
+ get isRateLimited() {
15
+ return this.status === 429;
16
+ }
17
+ /** True for 401/403 (auth/plan problems). */
18
+ get isAuthError() {
19
+ return this.status === 401 || this.status === 403;
20
+ }
21
+ /** True for 404. */
22
+ get isNotFound() {
23
+ return this.status === 404;
24
+ }
25
+ };
26
+ var WorkoutXConnectionError = class extends Error {
27
+ constructor(message, cause) {
28
+ super(message);
29
+ this.name = "WorkoutXConnectionError";
30
+ this.cause = cause;
31
+ }
32
+ };
33
+
34
+ // src/http.ts
35
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
36
+ function buildUrl(baseUrl, path, query) {
37
+ const url = new URL(baseUrl.replace(/\/$/, "") + path);
38
+ if (query) {
39
+ for (const [k, v] of Object.entries(query)) {
40
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
41
+ }
42
+ }
43
+ return url.toString();
44
+ }
45
+ var HttpClient = class {
46
+ constructor(cfg) {
47
+ this.cfg = cfg;
48
+ }
49
+ async request(path, opts = {}) {
50
+ const method = opts.method ?? "GET";
51
+ const url = buildUrl(this.cfg.baseUrl, path, opts.query);
52
+ const headers = {
53
+ Accept: opts.raw ? "*/*" : "application/json",
54
+ ...this.cfg.defaultHeaders
55
+ };
56
+ if (opts.body !== void 0) headers["Content-Type"] = "application/json";
57
+ if (opts.auth === "bearer") {
58
+ const token = await this.resolveBearer();
59
+ if (!token) {
60
+ throw new WorkoutXError({
61
+ status: 401,
62
+ path,
63
+ body: null,
64
+ code: "Unauthorized",
65
+ message: "Body-scan endpoints require authentication. Provide `scan.token` or `scan.email`/`scan.password` when constructing the client."
66
+ });
67
+ }
68
+ headers["Authorization"] = `Bearer ${token}`;
69
+ } else if (this.cfg.apiKey) {
70
+ headers["X-WorkoutX-Key"] = this.cfg.apiKey;
71
+ }
72
+ let lastErr;
73
+ for (let attempt = 0; attempt <= this.cfg.maxRetries; attempt++) {
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), this.cfg.timeout);
76
+ const signal = opts.signal ? anySignal([opts.signal, controller.signal]) : controller.signal;
77
+ let res;
78
+ try {
79
+ res = await this.cfg.fetch(url, {
80
+ method,
81
+ headers,
82
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
83
+ signal
84
+ });
85
+ } catch (err) {
86
+ clearTimeout(timer);
87
+ lastErr = err;
88
+ if (attempt < this.cfg.maxRetries) {
89
+ await sleep(backoffMs(attempt));
90
+ continue;
91
+ }
92
+ throw new WorkoutXConnectionError(
93
+ `Request to ${path} failed: ${err?.message ?? err}`,
94
+ err
95
+ );
96
+ }
97
+ clearTimeout(timer);
98
+ if ((res.status === 429 || res.status >= 500) && attempt < this.cfg.maxRetries) {
99
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
100
+ await sleep(retryAfter ?? backoffMs(attempt));
101
+ continue;
102
+ }
103
+ if (!res.ok) throw await toError(res, path);
104
+ if (opts.raw) return await res.arrayBuffer();
105
+ const text = await res.text();
106
+ if (!text) return void 0;
107
+ try {
108
+ return JSON.parse(text);
109
+ } catch {
110
+ return text;
111
+ }
112
+ }
113
+ throw lastErr instanceof Error ? lastErr : new WorkoutXConnectionError(`Request to ${path} failed after retries.`);
114
+ }
115
+ async resolveBearer() {
116
+ if (!this.cfg.getBearer) return void 0;
117
+ const v = this.cfg.getBearer();
118
+ return v instanceof Promise ? await v : v;
119
+ }
120
+ };
121
+ function backoffMs(attempt) {
122
+ return Math.min(2e3, 250 * 2 ** attempt) + Math.random() * 100;
123
+ }
124
+ function parseRetryAfter(header) {
125
+ if (!header) return void 0;
126
+ const secs = Number(header);
127
+ if (!Number.isNaN(secs)) return secs * 1e3;
128
+ const date = Date.parse(header);
129
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
130
+ }
131
+ async function toError(res, path) {
132
+ let body = null;
133
+ let parsed = {};
134
+ const text = await res.text().catch(() => "");
135
+ if (text) {
136
+ try {
137
+ parsed = JSON.parse(text);
138
+ body = parsed;
139
+ } catch {
140
+ body = text;
141
+ }
142
+ }
143
+ const message = parsed.message || parsed.error || `${res.status} ${res.statusText} for ${path}`;
144
+ return new WorkoutXError({
145
+ status: res.status,
146
+ path,
147
+ body,
148
+ message,
149
+ code: parsed.error,
150
+ tip: parsed.tip,
151
+ docs: parsed.docs
152
+ });
153
+ }
154
+ function anySignal(signals) {
155
+ const controller = new AbortController();
156
+ for (const s of signals) {
157
+ if (s.aborted) {
158
+ controller.abort();
159
+ break;
160
+ }
161
+ s.addEventListener("abort", () => controller.abort(), { once: true });
162
+ }
163
+ return controller.signal;
164
+ }
165
+
166
+ // src/resources/exercises.ts
167
+ var ExercisesResource = class {
168
+ constructor(http) {
169
+ this.http = http;
170
+ }
171
+ /** List exercises (paginated). */
172
+ list(params = {}) {
173
+ return this.http.request("/v1/exercises", { query: { ...params } });
174
+ }
175
+ /**
176
+ * Look up a single exercise by its exact numeric ID (e.g. "0001").
177
+ *
178
+ * Note: IDs are not sequential — there are gaps. To resolve a human name
179
+ * like "lunges", use {@link byName} instead. See {@link find} for a helper
180
+ * that tries an ID first and falls back to a name search.
181
+ */
182
+ get(id) {
183
+ return this.http.request(`/v1/exercises/${encodeURIComponent(id)}`);
184
+ }
185
+ /** Find exercises whose name contains the given text. */
186
+ byName(name, params = {}) {
187
+ return this.http.request(`/v1/exercises/name/${encodeURIComponent(name)}`, {
188
+ query: { ...params }
189
+ });
190
+ }
191
+ /** Filter exercises by body part (e.g. "back"). */
192
+ byBodyPart(bodyPart, params = {}) {
193
+ return this.http.request(`/v1/exercises/bodyPart/${encodeURIComponent(bodyPart)}`, {
194
+ query: { ...params }
195
+ });
196
+ }
197
+ /** Filter exercises by target muscle (e.g. "abs"). */
198
+ byTarget(target, params = {}) {
199
+ return this.http.request(`/v1/exercises/target/${encodeURIComponent(target)}`, {
200
+ query: { ...params }
201
+ });
202
+ }
203
+ /** Filter exercises by equipment (e.g. "barbell"). */
204
+ byEquipment(equipment, params = {}) {
205
+ return this.http.request(`/v1/exercises/equipment/${encodeURIComponent(equipment)}`, {
206
+ query: { ...params }
207
+ });
208
+ }
209
+ /** Multi-filter search (plan-gated: requires the `multiFilter` feature). */
210
+ search(params = {}) {
211
+ return this.http.request("/v1/exercises/search", { query: { ...params } });
212
+ }
213
+ /** Exercises similar to the given exercise ID. */
214
+ similar(id, params = {}) {
215
+ return this.http.request(`/v1/exercises/${encodeURIComponent(id)}/similar`, {
216
+ query: { ...params }
217
+ });
218
+ }
219
+ /** Alternatives that hit the same target with different equipment. */
220
+ alternatives(id, params = {}) {
221
+ return this.http.request(`/v1/exercises/${encodeURIComponent(id)}/alternatives`, {
222
+ query: { ...params }
223
+ });
224
+ }
225
+ /** Estimated calories burned for an exercise. */
226
+ calories(id, params = {}) {
227
+ return this.http.request(`/v1/exercises/${encodeURIComponent(id)}/calories`, {
228
+ query: { ...params }
229
+ });
230
+ }
231
+ /** Dataset changelog (plan-gated: requires the `datasetSync` feature). */
232
+ changes(params = {}) {
233
+ return this.http.request("/v1/exercises/changes", { query: { ...params } });
234
+ }
235
+ /** List of all body parts. */
236
+ bodyPartList() {
237
+ return this.http.request("/v1/exercises/bodyPartList");
238
+ }
239
+ /** List of all target muscles. */
240
+ targetList() {
241
+ return this.http.request("/v1/exercises/targetList");
242
+ }
243
+ /** List of all equipment types. */
244
+ equipmentList() {
245
+ return this.http.request("/v1/exercises/equipmentList");
246
+ }
247
+ /** List of all secondary muscles. */
248
+ secondaryMuscleList() {
249
+ return this.http.request("/v1/exercises/secondaryMuscleList");
250
+ }
251
+ /**
252
+ * Convenience resolver: treat `idOrName` as an ID first, and if that 404s,
253
+ * fall back to a name search and return the first match. Saves callers from
254
+ * the common "/v1/exercises/lunges → 404" mistake.
255
+ *
256
+ * Returns `null` when neither an ID nor a name match is found.
257
+ */
258
+ async find(idOrName) {
259
+ try {
260
+ return await this.get(idOrName);
261
+ } catch (err) {
262
+ if (!(err instanceof WorkoutXError) || !err.isNotFound) throw err;
263
+ }
264
+ const matches = await this.byName(idOrName, { limit: 1 });
265
+ return matches[0] ?? null;
266
+ }
267
+ };
268
+
269
+ // src/resources/gifs.ts
270
+ var GifsResource = class {
271
+ constructor(http) {
272
+ this.http = http;
273
+ }
274
+ /**
275
+ * Fetch an exercise GIF as raw bytes.
276
+ *
277
+ * Accepts both "0001" and "0001.gif". On the free plan the API serves a
278
+ * watermarked variant; paid plans get the original. Returns the binary
279
+ * payload — wrap it in `Blob`/`Buffer` as needed.
280
+ */
281
+ async get(filename) {
282
+ const name = /\.gif$/i.test(filename) ? filename : `${filename}.gif`;
283
+ return this.http.request(`/v1/gifs/${encodeURIComponent(name)}`, {
284
+ raw: true
285
+ });
286
+ }
287
+ /**
288
+ * Build the direct GIF URL (no request made). Useful for `<img src>`.
289
+ * The API key still needs to be supplied — by default via the `api-key`
290
+ * query param, which works for `<img>` tags that can't set headers.
291
+ */
292
+ url(filename, opts) {
293
+ const name = /\.gif$/i.test(filename) ? filename : `${filename}.gif`;
294
+ const u = new URL(`${opts.baseUrl.replace(/\/$/, "")}/v1/gifs/${encodeURIComponent(name)}`);
295
+ if (opts.apiKey) u.searchParams.set("api-key", opts.apiKey);
296
+ return u.toString();
297
+ }
298
+ };
299
+
300
+ // src/resources/workout.ts
301
+ var WorkoutResource = class {
302
+ constructor(http) {
303
+ this.http = http;
304
+ }
305
+ /** Generate a single workout (plan-gated: requires `workoutGenerator`). */
306
+ generate(params = {}) {
307
+ return this.http.request("/v1/workout/generate", { query: { ...params } });
308
+ }
309
+ /** Generate a multi-week program (plan-gated: requires `workoutPrograms`). */
310
+ program(params = {}) {
311
+ return this.http.request("/v1/workout/program", { query: { ...params } });
312
+ }
313
+ };
314
+
315
+ // src/resources/supplements.ts
316
+ var SupplementsResource = class {
317
+ constructor(http) {
318
+ this.http = http;
319
+ }
320
+ /** List supplements (paginated). */
321
+ list(params = {}) {
322
+ return this.http.request("/v1/supplements", { query: { ...params } });
323
+ }
324
+ /** Available supplement filters. */
325
+ filters() {
326
+ return this.http.request("/v1/supplements/filters");
327
+ }
328
+ /** Build a recommended stack (plan-gated: requires `supplementStacks`). */
329
+ stack(params = {}) {
330
+ return this.http.request("/v1/supplements/stack", { query: { ...params } });
331
+ }
332
+ /** Supplements recommended for a given exercise. */
333
+ forExercise(exerciseId) {
334
+ return this.http.request(`/v1/supplements/exercise/${encodeURIComponent(exerciseId)}`);
335
+ }
336
+ /** Get a single supplement by ID. */
337
+ get(id) {
338
+ return this.http.request(`/v1/supplements/${encodeURIComponent(id)}`);
339
+ }
340
+ };
341
+
342
+ // src/resources/scan.ts
343
+ var ScanResource = class {
344
+ constructor(http) {
345
+ this.http = http;
346
+ }
347
+ /** Body-scan credit balance + usage stats for the current user. */
348
+ credits() {
349
+ return this.http.request("/v1/scan/credits", { auth: "bearer" });
350
+ }
351
+ /** Current user's body-scan profile. */
352
+ me() {
353
+ return this.http.request("/v1/scan/me", { auth: "bearer" });
354
+ }
355
+ /** Paginated scan history. */
356
+ history(params = {}) {
357
+ return this.http.request("/v1/scan/history", { auth: "bearer", query: { ...params } });
358
+ }
359
+ /** List the user's body-scan API keys. */
360
+ listKeys() {
361
+ return this.http.request("/v1/scan/keys", { auth: "bearer" });
362
+ }
363
+ /** Create a body-scan API key. */
364
+ createKey(name) {
365
+ return this.http.request("/v1/scan/keys", { auth: "bearer", method: "POST", body: { name } });
366
+ }
367
+ /** Delete a body-scan API key by ID. */
368
+ deleteKey(id) {
369
+ return this.http.request(`/v1/scan/keys/${encodeURIComponent(id)}`, {
370
+ auth: "bearer",
371
+ method: "DELETE"
372
+ });
373
+ }
374
+ /** Create a Stripe checkout session for a credit pack. */
375
+ checkout(body = {}) {
376
+ return this.http.request("/v1/scan/checkout", { auth: "bearer", method: "POST", body });
377
+ }
378
+ /** Public list of available credit packs (no auth required). */
379
+ packs() {
380
+ return this.http.request("/v1/scan/packs");
381
+ }
382
+ };
383
+
384
+ // src/resources/auth.ts
385
+ var AuthResource = class {
386
+ constructor(http, onLogin) {
387
+ this.http = http;
388
+ this.onLogin = onLogin;
389
+ }
390
+ /** Create a new account. Sends a verification email; no token is issued yet. */
391
+ register(params) {
392
+ return this.http.request("/v1/auth/register", { method: "POST", body: params });
393
+ }
394
+ /** Log in and receive a Bearer token (also used by `scan.*`). */
395
+ async login(email, password) {
396
+ const result = await this.http.request("/v1/auth/login", {
397
+ method: "POST",
398
+ body: { email, password }
399
+ });
400
+ this.onLogin?.(result.token);
401
+ return result;
402
+ }
403
+ /** Get the current user's profile. */
404
+ me() {
405
+ return this.http.request("/v1/auth/me", { auth: "bearer" });
406
+ }
407
+ /** Update the current user's display name. */
408
+ updateMe(name) {
409
+ return this.http.request("/v1/auth/me", { auth: "bearer", method: "PATCH", body: { name } });
410
+ }
411
+ /** Change the current user's password. */
412
+ changePassword(currentPassword, newPassword) {
413
+ return this.http.request("/v1/auth/change-password", {
414
+ auth: "bearer",
415
+ method: "POST",
416
+ body: { currentPassword, newPassword }
417
+ });
418
+ }
419
+ /** List the current user's API keys (exercises/gifs/workout/supplements product). */
420
+ listKeys() {
421
+ return this.http.request("/v1/auth/keys", { auth: "bearer" });
422
+ }
423
+ /** Create a new API key. */
424
+ createKey(name) {
425
+ return this.http.request("/v1/auth/keys", { auth: "bearer", method: "POST", body: { name } });
426
+ }
427
+ /** Delete an API key by ID. */
428
+ deleteKey(id) {
429
+ return this.http.request(`/v1/auth/keys/${encodeURIComponent(id)}`, {
430
+ auth: "bearer",
431
+ method: "DELETE"
432
+ });
433
+ }
434
+ /** Change the plan assigned to an API key. */
435
+ updateKeyPlan(id, planId) {
436
+ return this.http.request(`/v1/auth/keys/${encodeURIComponent(id)}/plan`, {
437
+ auth: "bearer",
438
+ method: "PATCH",
439
+ body: { planId }
440
+ });
441
+ }
442
+ /** Usage statistics for the current user (optionally filtered to one API key). */
443
+ usage(params = {}) {
444
+ return this.http.request("/v1/auth/usage", { auth: "bearer", query: { ...params } });
445
+ }
446
+ /** Request a password-reset email. Always succeeds (no email enumeration). */
447
+ forgotPassword(email) {
448
+ return this.http.request("/v1/auth/forgot-password", { method: "POST", body: { email } });
449
+ }
450
+ /** Complete a password reset using the token emailed by `forgotPassword`. */
451
+ resetPassword(token, password) {
452
+ return this.http.request("/v1/auth/reset-password", {
453
+ method: "POST",
454
+ body: { token, password }
455
+ });
456
+ }
457
+ /** Verify an email address using the token from the verification email. */
458
+ verifyEmail(token) {
459
+ return this.http.request("/v1/auth/verify-email", { query: { token } });
460
+ }
461
+ /** Resend the verification email. Always succeeds (no email enumeration). */
462
+ resendVerification(email) {
463
+ return this.http.request("/v1/auth/resend-verification", {
464
+ method: "POST",
465
+ body: { email }
466
+ });
467
+ }
468
+ /** Get the current user's registered webhook URL. */
469
+ getWebhook() {
470
+ return this.http.request("/v1/auth/webhook", { auth: "bearer" });
471
+ }
472
+ /** Set (or clear, with `null`) the webhook URL. */
473
+ setWebhook(webhookUrl) {
474
+ return this.http.request("/v1/auth/webhook", {
475
+ auth: "bearer",
476
+ method: "PATCH",
477
+ body: { webhookUrl }
478
+ });
479
+ }
480
+ /** Send a test event to the registered webhook URL. */
481
+ testWebhook() {
482
+ return this.http.request("/v1/auth/webhook/test", { auth: "bearer", method: "POST" });
483
+ }
484
+ };
485
+
486
+ // src/resources/billing.ts
487
+ var BillingResource = class {
488
+ constructor(http) {
489
+ this.http = http;
490
+ }
491
+ /** Current subscription status, plan, and renewal date for the logged-in user. */
492
+ status() {
493
+ return this.http.request("/v1/billing/status", { auth: "bearer" });
494
+ }
495
+ /** Create a Stripe Checkout session for upgrading to a paid plan. */
496
+ checkout(planId, billingCycle = "monthly") {
497
+ return this.http.request("/v1/billing/checkout", {
498
+ auth: "bearer",
499
+ method: "POST",
500
+ body: { planId, billingCycle }
501
+ });
502
+ }
503
+ /** Open the Stripe Customer Portal for the logged-in user to manage/cancel their subscription. */
504
+ portal() {
505
+ return this.http.request("/v1/billing/portal", { auth: "bearer", method: "POST" });
506
+ }
507
+ };
508
+
509
+ // src/client.ts
510
+ var DEFAULT_BASE_URL = "https://api.workoutxapp.com";
511
+ var WorkoutX = class {
512
+ constructor(opts = {}) {
513
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
514
+ if (typeof fetchImpl !== "function") {
515
+ throw new Error(
516
+ "No global fetch available. Use Node 18+ or pass a `fetch` implementation in options."
517
+ );
518
+ }
519
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
520
+ this.apiKey = opts.apiKey;
521
+ this.scanAuth = opts.scan;
522
+ if (opts.scan && "token" in opts.scan) this.cachedToken = opts.scan.token;
523
+ this.http = new HttpClient({
524
+ baseUrl: this.baseUrl,
525
+ apiKey: this.apiKey,
526
+ getBearer: () => this.getBearerToken(),
527
+ timeout: opts.timeout ?? 3e4,
528
+ maxRetries: opts.maxRetries ?? 2,
529
+ defaultHeaders: { "User-Agent": "workoutx-sdk-js/0.1.0", ...opts.headers },
530
+ fetch: fetchImpl.bind(globalThis)
531
+ });
532
+ this.exercises = new ExercisesResource(this.http);
533
+ this.gifs = new GifsResource(this.http);
534
+ this.workout = new WorkoutResource(this.http);
535
+ this.supplements = new SupplementsResource(this.http);
536
+ this.scan = new ScanResource(this.http);
537
+ this.auth = new AuthResource(this.http, (token) => this.setScanToken(token));
538
+ this.billing = new BillingResource(this.http);
539
+ }
540
+ /** Build a direct GIF URL with the API key baked into the query string. */
541
+ gifUrl(filename) {
542
+ return this.gifs.url(filename, { baseUrl: this.baseUrl, apiKey: this.apiKey });
543
+ }
544
+ /** Manually set/replace the body-scan Bearer token. */
545
+ setScanToken(token) {
546
+ this.cachedToken = token;
547
+ }
548
+ /** Resolve a Bearer token, logging in with email/password if needed. */
549
+ async getBearerToken() {
550
+ if (this.cachedToken) return this.cachedToken;
551
+ if (!this.scanAuth || !("email" in this.scanAuth)) return void 0;
552
+ if (!this.loginInFlight) {
553
+ const creds = this.scanAuth;
554
+ this.loginInFlight = this.auth.login(creds.email, creds.password).then((res) => res.token).finally(() => {
555
+ this.loginInFlight = void 0;
556
+ });
557
+ }
558
+ return this.loginInFlight;
559
+ }
560
+ };
561
+ export {
562
+ AuthResource,
563
+ BillingResource,
564
+ DEFAULT_BASE_URL,
565
+ ExercisesResource,
566
+ GifsResource,
567
+ ScanResource,
568
+ SupplementsResource,
569
+ WorkoutResource,
570
+ WorkoutX,
571
+ WorkoutXConnectionError,
572
+ WorkoutXError
573
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@workoutx/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript/JavaScript SDK for the WorkoutX API — exercises, GIFs, workouts, supplements, and body scan.",
5
+ "license": "MIT",
6
+ "author": "WorkoutX",
7
+ "homepage": "https://workoutxapp.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/furkanuruk/workoutxapi.git",
11
+ "directory": "sdk"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/furkanuruk/workoutxapi/issues"
15
+ },
16
+ "keywords": [
17
+ "workoutx",
18
+ "fitness",
19
+ "exercise",
20
+ "exercisedb",
21
+ "workout",
22
+ "supplements",
23
+ "body-scan",
24
+ "api",
25
+ "sdk"
26
+ ],
27
+ "type": "module",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "CHANGELOG.md",
42
+ "LICENSE"
43
+ ],
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "prepublishOnly": "npm run build"
53
+ },
54
+ "devDependencies": {
55
+ "tsup": "^8.0.0",
56
+ "typescript": "^5.4.0",
57
+ "vitest": "^2.0.0"
58
+ }
59
+ }