fresh-squeezy 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/cli.js ADDED
@@ -0,0 +1,954 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/main.ts
4
+ import { Command } from "commander";
5
+ import dotenv from "dotenv";
6
+
7
+ // src/core/errors.ts
8
+ var FreshSqueezyError = class _FreshSqueezyError extends Error {
9
+ code;
10
+ status;
11
+ detail;
12
+ constructor(opts) {
13
+ super(opts.message);
14
+ this.name = "FreshSqueezyError";
15
+ this.code = opts.code;
16
+ this.status = opts.status;
17
+ this.detail = opts.detail;
18
+ Object.setPrototypeOf(this, _FreshSqueezyError.prototype);
19
+ }
20
+ };
21
+
22
+ // src/core/config.ts
23
+ var DEFAULT_BASE_URL = "https://api.lemonsqueezy.com";
24
+ var ENV_KEYS = {
25
+ apiKey: "LEMON_SQUEEZY_API_KEY",
26
+ storeId: "LEMON_SQUEEZY_STORE_ID",
27
+ mode: "LEMON_SQUEEZY_MODE"
28
+ };
29
+ function resolveConfig(input = {}) {
30
+ const apiKey = input.apiKey ?? process.env[ENV_KEYS.apiKey];
31
+ if (!apiKey) {
32
+ throw new FreshSqueezyError({
33
+ code: "MISSING_API_KEY",
34
+ message: `No API key provided. Pass \`apiKey\` or set ${ENV_KEYS.apiKey}.`
35
+ });
36
+ }
37
+ const mode = normalizeMode(input.mode ?? process.env[ENV_KEYS.mode] ?? "test");
38
+ const storeIdRaw = input.storeId ?? process.env[ENV_KEYS.storeId];
39
+ return {
40
+ apiKey,
41
+ storeId: storeIdRaw == null ? void 0 : String(storeIdRaw),
42
+ mode,
43
+ baseUrl: input.baseUrl ?? DEFAULT_BASE_URL,
44
+ fetch: input.fetch ?? globalThis.fetch
45
+ };
46
+ }
47
+ function normalizeMode(value) {
48
+ if (value === "test" || value === "live") return value;
49
+ throw new FreshSqueezyError({
50
+ code: "INVALID_MODE",
51
+ message: `Mode must be "test" or "live", got "${value}".`
52
+ });
53
+ }
54
+
55
+ // src/core/http.ts
56
+ var HttpClient = class {
57
+ constructor(config) {
58
+ this.config = config;
59
+ }
60
+ config;
61
+ async request(options) {
62
+ const url = this.buildUrl(options.path, options.query);
63
+ const headers = {
64
+ Authorization: `Bearer ${this.config.apiKey}`,
65
+ Accept: "application/vnd.api+json"
66
+ };
67
+ if (options.body !== void 0) {
68
+ headers["Content-Type"] = "application/vnd.api+json";
69
+ }
70
+ let response;
71
+ try {
72
+ response = await this.config.fetch(url, {
73
+ method: options.method ?? "GET",
74
+ headers,
75
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
76
+ signal: options.signal
77
+ });
78
+ } catch (cause) {
79
+ throw new FreshSqueezyError({
80
+ code: "NETWORK_ERROR",
81
+ message: cause instanceof Error ? cause.message : "Network request failed",
82
+ detail: cause
83
+ });
84
+ }
85
+ const text = await response.text();
86
+ const parsed = text.length > 0 ? safeJsonParse(text) : void 0;
87
+ if (!response.ok) {
88
+ throw toApiError(response.status, parsed);
89
+ }
90
+ return parsed;
91
+ }
92
+ /**
93
+ * Fetch a single JSON:API resource and return its `data` object.
94
+ */
95
+ async getResource(path2) {
96
+ const doc = await this.request({ path: path2 });
97
+ return doc.data;
98
+ }
99
+ /**
100
+ * Fetch a JSON:API collection and return its `data` array.
101
+ * Pagination is the caller's responsibility — use `meta.page` on the raw
102
+ * request for multi-page traversal.
103
+ */
104
+ async getCollection(path2, query) {
105
+ const doc = await this.request({ path: path2, query });
106
+ return doc.data;
107
+ }
108
+ buildUrl(path2, query) {
109
+ const url = new URL(path2, this.config.baseUrl);
110
+ if (query) {
111
+ for (const [key, value] of Object.entries(query)) {
112
+ if (value === void 0) continue;
113
+ url.searchParams.append(key, String(value));
114
+ }
115
+ }
116
+ return url.toString();
117
+ }
118
+ };
119
+ function safeJsonParse(text) {
120
+ try {
121
+ return JSON.parse(text);
122
+ } catch {
123
+ return text;
124
+ }
125
+ }
126
+ function toApiError(status, body) {
127
+ const errors = extractJsonApiErrors(body);
128
+ const first = errors[0];
129
+ const code = status === 401 ? "UNAUTHORIZED" : status === 404 ? "NOT_FOUND" : status === 429 ? "RATE_LIMITED" : first?.code ?? `HTTP_${status}`;
130
+ const message = first?.detail ?? first?.title ?? `Lemon Squeezy request failed with status ${status}`;
131
+ return new FreshSqueezyError({ code, status, message, detail: body });
132
+ }
133
+ function extractJsonApiErrors(body) {
134
+ if (!body || typeof body !== "object") return [];
135
+ const errors = body.errors;
136
+ if (!Array.isArray(errors)) return [];
137
+ return errors.filter(
138
+ (entry) => typeof entry === "object" && entry !== null
139
+ );
140
+ }
141
+
142
+ // src/resources/users.ts
143
+ async function getAuthenticatedUser(http) {
144
+ return http.request({ path: "/v1/users/me" });
145
+ }
146
+
147
+ // src/resources/stores.ts
148
+ async function getStore(http, storeId) {
149
+ return http.getResource(`/v1/stores/${storeId}`);
150
+ }
151
+ async function listStores(http) {
152
+ return http.getCollection("/v1/stores");
153
+ }
154
+
155
+ // src/validate/rules.ts
156
+ var ISSUE_CODES = {
157
+ AUTH_FAILED: "AUTH_FAILED",
158
+ MODE_MISMATCH: "MODE_MISMATCH",
159
+ STORE_NOT_FOUND: "STORE_NOT_FOUND",
160
+ STORE_NOT_OWNED: "STORE_NOT_OWNED",
161
+ PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
162
+ PRODUCT_WRONG_STORE: "PRODUCT_WRONG_STORE",
163
+ PRODUCT_UNPUBLISHED: "PRODUCT_UNPUBLISHED",
164
+ PRODUCT_NO_BUY_URL: "PRODUCT_NO_BUY_URL",
165
+ VARIANT_UNPUBLISHED: "VARIANT_UNPUBLISHED",
166
+ VARIANT_MISSING: "VARIANT_MISSING",
167
+ WEBHOOK_NOT_FOUND: "WEBHOOK_NOT_FOUND",
168
+ WEBHOOK_EVENTS_MISSING: "WEBHOOK_EVENTS_MISSING",
169
+ WEBHOOK_OPTIONAL_EVENTS: "WEBHOOK_OPTIONAL_EVENTS",
170
+ NETWORK_ERROR: "NETWORK_ERROR",
171
+ UNKNOWN: "UNKNOWN"
172
+ };
173
+ function issue(code, severity, message, extras = {}) {
174
+ const base = { code, severity, message };
175
+ if (extras.suggestedFix !== void 0) base.suggestedFix = extras.suggestedFix;
176
+ if (extras.context !== void 0) base.context = extras.context;
177
+ return base;
178
+ }
179
+ function isOk(issues) {
180
+ return !issues.some((entry) => entry.severity === "error");
181
+ }
182
+ function buildResult(name, mode, issues, resource) {
183
+ const result = {
184
+ name,
185
+ ok: isOk(issues),
186
+ mode,
187
+ issues
188
+ };
189
+ if (resource !== void 0) result.resource = resource;
190
+ return result;
191
+ }
192
+
193
+ // src/validate/connection.ts
194
+ async function validateConnection(http, mode) {
195
+ const issues = [];
196
+ try {
197
+ const userDoc = await getAuthenticatedUser(http);
198
+ const stores = await listStores(http);
199
+ const actualMode = resolveActualMode(userDoc.meta?.test_mode);
200
+ const summary = {
201
+ user: userDoc.data.attributes,
202
+ storeCount: stores.length,
203
+ storeIds: stores.map((store) => store.id),
204
+ declaredMode: mode,
205
+ ...actualMode ? { actualMode } : {}
206
+ };
207
+ if (actualMode && actualMode !== mode) {
208
+ issues.push(
209
+ issue(
210
+ ISSUE_CODES.MODE_MISMATCH,
211
+ "error",
212
+ `API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
213
+ {
214
+ suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
215
+ context: { declared: mode, actual: actualMode }
216
+ }
217
+ )
218
+ );
219
+ }
220
+ if (stores.length === 0) {
221
+ issues.push(
222
+ issue(
223
+ ISSUE_CODES.STORE_NOT_FOUND,
224
+ "warning",
225
+ "API key authenticated but no stores are reachable.",
226
+ { suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
227
+ )
228
+ );
229
+ }
230
+ return buildResult("connection", mode, issues, summary);
231
+ } catch (err) {
232
+ issues.push(toConnectionIssue(err));
233
+ return buildResult("connection", mode, issues);
234
+ }
235
+ }
236
+ function resolveActualMode(testMode) {
237
+ if (testMode === true) return "test";
238
+ if (testMode === false) return "live";
239
+ return void 0;
240
+ }
241
+ function toConnectionIssue(err) {
242
+ if (err instanceof FreshSqueezyError) {
243
+ if (err.code === "UNAUTHORIZED") {
244
+ return issue(ISSUE_CODES.AUTH_FAILED, "error", "API key rejected by Lemon Squeezy.", {
245
+ suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api.",
246
+ context: { status: err.status ?? null }
247
+ });
248
+ }
249
+ if (err.code === "NETWORK_ERROR") {
250
+ return issue(ISSUE_CODES.NETWORK_ERROR, "error", `Could not reach Lemon Squeezy: ${err.message}`);
251
+ }
252
+ return issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
253
+ context: { status: err.status ?? null, code: err.code }
254
+ });
255
+ }
256
+ const message = err instanceof Error ? err.message : "Unknown error";
257
+ return issue(ISSUE_CODES.UNKNOWN, "error", message);
258
+ }
259
+
260
+ // src/validate/store.ts
261
+ async function validateStore(http, mode, storeId) {
262
+ const issues = [];
263
+ try {
264
+ const store = await getStore(http, storeId);
265
+ return buildResult("store", mode, issues, store.attributes);
266
+ } catch (err) {
267
+ if (err instanceof FreshSqueezyError && err.status === 404) {
268
+ issues.push(
269
+ issue(
270
+ ISSUE_CODES.STORE_NOT_FOUND,
271
+ "error",
272
+ `Store ${storeId} not found with the current API key.`,
273
+ {
274
+ suggestedFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
275
+ context: { storeId: String(storeId) }
276
+ }
277
+ )
278
+ );
279
+ return buildResult("store", mode, issues);
280
+ }
281
+ if (err instanceof FreshSqueezyError) {
282
+ issues.push(
283
+ issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
284
+ context: { status: err.status ?? null, code: err.code }
285
+ })
286
+ );
287
+ return buildResult("store", mode, issues);
288
+ }
289
+ const message = err instanceof Error ? err.message : "Unknown error";
290
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
291
+ return buildResult("store", mode, issues);
292
+ }
293
+ }
294
+
295
+ // src/resources/products.ts
296
+ async function getProduct(http, productId) {
297
+ return http.getResource(`/v1/products/${productId}`);
298
+ }
299
+
300
+ // src/resources/variants.ts
301
+ async function listVariantsForProduct(http, productId) {
302
+ return http.getCollection("/v1/variants", {
303
+ "filter[product_id]": String(productId)
304
+ });
305
+ }
306
+
307
+ // src/validate/product.ts
308
+ async function validateProduct(http, mode, options) {
309
+ const issues = [];
310
+ let product;
311
+ try {
312
+ product = await getProduct(http, options.productId);
313
+ } catch (err) {
314
+ if (err instanceof FreshSqueezyError && err.status === 404) {
315
+ issues.push(
316
+ issue(
317
+ ISSUE_CODES.PRODUCT_NOT_FOUND,
318
+ "error",
319
+ `Product ${options.productId} not found.`,
320
+ {
321
+ suggestedFix: "Verify the product ID in the Lemon Squeezy dashboard.",
322
+ context: { productId: String(options.productId) }
323
+ }
324
+ )
325
+ );
326
+ return buildResult("product", mode, issues);
327
+ }
328
+ const message = err instanceof Error ? err.message : "Unknown error";
329
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
330
+ return buildResult("product", mode, issues);
331
+ }
332
+ const attrs = product.attributes;
333
+ if (options.expectedStoreId !== void 0) {
334
+ const expected = String(options.expectedStoreId);
335
+ const actual = String(attrs.store_id);
336
+ if (expected !== actual) {
337
+ issues.push(
338
+ issue(
339
+ ISSUE_CODES.PRODUCT_WRONG_STORE,
340
+ "error",
341
+ `Product belongs to store ${actual}, expected ${expected}.`,
342
+ {
343
+ suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores.",
344
+ context: { expectedStoreId: expected, actualStoreId: actual }
345
+ }
346
+ )
347
+ );
348
+ }
349
+ }
350
+ if (attrs.status !== "published") {
351
+ issues.push(
352
+ issue(
353
+ ISSUE_CODES.PRODUCT_UNPUBLISHED,
354
+ "error",
355
+ `Product is in "${attrs.status}" state, not "published".`,
356
+ {
357
+ suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
358
+ context: { status: attrs.status }
359
+ }
360
+ )
361
+ );
362
+ }
363
+ if (!attrs.buy_now_url) {
364
+ issues.push(
365
+ issue(
366
+ ISSUE_CODES.PRODUCT_NO_BUY_URL,
367
+ "warning",
368
+ "Product has no buy-now URL. Hosted checkout may be disabled.",
369
+ { suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
370
+ )
371
+ );
372
+ }
373
+ try {
374
+ const variants = await listVariantsForProduct(http, options.productId);
375
+ if (variants.length === 0) {
376
+ issues.push(
377
+ issue(
378
+ ISSUE_CODES.VARIANT_MISSING,
379
+ "error",
380
+ "Product has no variants. Customers cannot purchase it.",
381
+ { suggestedFix: "Add at least one variant in the product configuration." }
382
+ )
383
+ );
384
+ } else if (!variants.some((variant) => variant.attributes.status === "published")) {
385
+ issues.push(
386
+ issue(
387
+ ISSUE_CODES.VARIANT_UNPUBLISHED,
388
+ "error",
389
+ "Product has variants but none are published.",
390
+ { suggestedFix: "Publish at least one variant." }
391
+ )
392
+ );
393
+ }
394
+ } catch (err) {
395
+ const message = err instanceof Error ? err.message : "Unknown error fetching variants";
396
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
397
+ }
398
+ return buildResult("product", mode, issues, attrs);
399
+ }
400
+
401
+ // src/resources/webhooks.ts
402
+ async function listWebhooksForStore(http, storeId) {
403
+ return http.getCollection("/v1/webhooks", {
404
+ "filter[store_id]": String(storeId)
405
+ });
406
+ }
407
+
408
+ // src/support/manifest.ts
409
+ var RECOMMENDED_WEBHOOK_EVENTS = [
410
+ "order_created",
411
+ "order_refunded",
412
+ "subscription_created",
413
+ "subscription_updated",
414
+ "subscription_cancelled",
415
+ "subscription_resumed",
416
+ "subscription_expired",
417
+ "subscription_payment_success",
418
+ "subscription_payment_failed"
419
+ ];
420
+ var OPTIONAL_WEBHOOK_EVENTS = [
421
+ "customer_updated",
422
+ "affiliate_activated",
423
+ "license_key_created",
424
+ "license_key_updated"
425
+ ];
426
+
427
+ // src/validate/webhook.ts
428
+ async function validateWebhook(http, mode, options) {
429
+ const issues = [];
430
+ let webhooks;
431
+ try {
432
+ webhooks = await listWebhooksForStore(http, options.storeId);
433
+ } catch (err) {
434
+ if (err instanceof FreshSqueezyError) {
435
+ issues.push(
436
+ issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
437
+ context: { status: err.status ?? null, code: err.code }
438
+ })
439
+ );
440
+ return buildResult("webhook", mode, issues);
441
+ }
442
+ const message = err instanceof Error ? err.message : "Unknown error";
443
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
444
+ return buildResult("webhook", mode, issues);
445
+ }
446
+ const match = webhooks.find((webhook) => normalizeUrl(webhook.attributes.url) === normalizeUrl(options.url));
447
+ if (!match) {
448
+ issues.push(
449
+ issue(
450
+ ISSUE_CODES.WEBHOOK_NOT_FOUND,
451
+ "error",
452
+ `No webhook registered for URL ${options.url} on store ${options.storeId}.`,
453
+ {
454
+ suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
455
+ context: { storeId: String(options.storeId), url: options.url }
456
+ }
457
+ )
458
+ );
459
+ return buildResult("webhook", mode, issues);
460
+ }
461
+ const subscribed = new Set(match.attributes.events);
462
+ const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
463
+ const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
464
+ if (missingRecommended.length > 0) {
465
+ issues.push(
466
+ issue(
467
+ ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
468
+ "error",
469
+ `Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
470
+ {
471
+ suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
472
+ context: { missing: missingRecommended.join(",") }
473
+ }
474
+ )
475
+ );
476
+ }
477
+ if (missingOptional.length > 0) {
478
+ issues.push(
479
+ issue(
480
+ ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
481
+ "info",
482
+ `Optional events not subscribed: ${missingOptional.join(", ")}.`,
483
+ { context: { missing: missingOptional.join(",") } }
484
+ )
485
+ );
486
+ }
487
+ return buildResult("webhook", mode, issues, match.attributes);
488
+ }
489
+ function normalizeUrl(raw) {
490
+ return raw.replace(/\/+$/, "").toLowerCase();
491
+ }
492
+
493
+ // src/validate/doctor.ts
494
+ async function doctor(http, mode, options = {}) {
495
+ const results = [];
496
+ const connection = await validateConnection(http, mode);
497
+ results.push(connection);
498
+ if (!connection.ok) {
499
+ return { ok: false, mode, results };
500
+ }
501
+ if (options.storeId !== void 0) {
502
+ results.push(await validateStore(http, mode, options.storeId));
503
+ }
504
+ if (options.productId !== void 0) {
505
+ results.push(
506
+ await validateProduct(http, mode, {
507
+ productId: options.productId,
508
+ expectedStoreId: options.storeId
509
+ })
510
+ );
511
+ }
512
+ if (options.storeId !== void 0 && options.webhookUrl !== void 0) {
513
+ results.push(
514
+ await validateWebhook(http, mode, {
515
+ storeId: options.storeId,
516
+ url: options.webhookUrl
517
+ })
518
+ );
519
+ }
520
+ const ok = results.every((result) => result.ok);
521
+ return { ok, mode, results };
522
+ }
523
+
524
+ // src/createFreshSqueezy.ts
525
+ function createFreshSqueezy(config = {}) {
526
+ const resolved = resolveConfig(config);
527
+ const http = new HttpClient(resolved);
528
+ return {
529
+ mode: resolved.mode,
530
+ request: (options) => http.request(options),
531
+ validateConnection: () => validateConnection(http, resolved.mode),
532
+ validateStore: (storeId) => validateStore(http, resolved.mode, storeId),
533
+ validateProduct: (options) => validateProduct(http, resolved.mode, options),
534
+ validateWebhook: (options) => validateWebhook(http, resolved.mode, options),
535
+ doctor: (options) => doctor(http, resolved.mode, {
536
+ storeId: options?.storeId ?? resolved.storeId,
537
+ productId: options?.productId,
538
+ webhookUrl: options?.webhookUrl
539
+ })
540
+ };
541
+ }
542
+
543
+ // src/cli/render.ts
544
+ import chalk from "chalk";
545
+ function renderResult(result) {
546
+ const lines = [];
547
+ const badge = result.ok ? chalk.green("PASS") : chalk.red("FAIL");
548
+ const mode = chalk.dim(`[${result.mode}]`);
549
+ lines.push(`${badge} ${mode} ${chalk.bold(result.name)}`);
550
+ for (const issue2 of result.issues) {
551
+ lines.push(` ${renderIssueLine(issue2)}`);
552
+ if (issue2.suggestedFix) {
553
+ lines.push(` ${chalk.dim("fix:")} ${issue2.suggestedFix}`);
554
+ }
555
+ }
556
+ return lines.join("\n");
557
+ }
558
+ function renderReport(report) {
559
+ const header = report.ok ? chalk.green.bold("fresh-squeezy doctor: OK") : chalk.red.bold("fresh-squeezy doctor: FAILED");
560
+ const body = report.results.map(renderResult).join("\n\n");
561
+ return `${header} ${chalk.dim(`(mode: ${report.mode})`)}
562
+
563
+ ${body}`;
564
+ }
565
+ function renderIssueLine(issue2) {
566
+ const label = issue2.severity === "error" ? chalk.red("\u2717 error") : issue2.severity === "warning" ? chalk.yellow("! warn ") : chalk.blue("i info ");
567
+ return `${label} ${chalk.gray(`[${issue2.code}]`)} ${issue2.message}`;
568
+ }
569
+
570
+ // src/cli/prompts.ts
571
+ import inquirer from "inquirer";
572
+ async function askForCredentials() {
573
+ const answers = await inquirer.prompt([
574
+ {
575
+ type: "password",
576
+ name: "apiKey",
577
+ message: "Paste your Lemon Squeezy API key:",
578
+ mask: "*",
579
+ validate: (value) => value.trim().length > 0 ? true : "API key is required."
580
+ },
581
+ {
582
+ type: "list",
583
+ name: "mode",
584
+ message: "Which mode does this key belong to?",
585
+ choices: [
586
+ { name: "test \u2014 sandbox / development", value: "test" },
587
+ { name: "live \u2014 production charges", value: "live" }
588
+ ],
589
+ default: "test"
590
+ }
591
+ ]);
592
+ return answers;
593
+ }
594
+ async function pickStore(choices) {
595
+ const { storeId } = await inquirer.prompt([
596
+ {
597
+ type: "list",
598
+ name: "storeId",
599
+ message: "Pick a store to validate against:",
600
+ choices: choices.map((entry) => ({
601
+ name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
602
+ value: entry.id
603
+ }))
604
+ }
605
+ ]);
606
+ return storeId;
607
+ }
608
+ async function pickStores(choices) {
609
+ const { storeIds } = await inquirer.prompt([
610
+ {
611
+ type: "checkbox",
612
+ name: "storeIds",
613
+ message: "Pick one or more stores (space to toggle, enter to confirm):",
614
+ choices: choices.map((entry, index) => ({
615
+ name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
616
+ value: entry.id,
617
+ checked: index === 0
618
+ })),
619
+ validate: (values) => Array.isArray(values) && values.length > 0 ? true : "Pick at least one store."
620
+ }
621
+ ]);
622
+ return storeIds;
623
+ }
624
+ async function confirmWriteEnvFile(path2) {
625
+ const { confirm } = await inquirer.prompt([
626
+ {
627
+ type: "confirm",
628
+ name: "confirm",
629
+ message: `Write these values to ${path2}?`,
630
+ default: true
631
+ }
632
+ ]);
633
+ return confirm;
634
+ }
635
+
636
+ // src/cli/resolveStores.ts
637
+ async function resolveStores(client, input) {
638
+ if (input.storeIds && input.storeIds.length > 0) {
639
+ return { storeIds: dedupe(input.storeIds), skipped: false };
640
+ }
641
+ if (input.allStores) {
642
+ const ids2 = await fetchReachableStoreIds(client);
643
+ if (ids2.length === 0) {
644
+ throw new FreshSqueezyError({
645
+ code: "NO_STORES",
646
+ message: "No stores reachable with this API key."
647
+ });
648
+ }
649
+ return { storeIds: ids2, skipped: false };
650
+ }
651
+ if (!input.isInteractive) {
652
+ return { storeIds: [], skipped: true };
653
+ }
654
+ const ids = await fetchReachableStoreIds(client);
655
+ if (ids.length === 0) {
656
+ throw new FreshSqueezyError({
657
+ code: "NO_STORES",
658
+ message: "No stores reachable with this API key."
659
+ });
660
+ }
661
+ if (ids.length === 1) {
662
+ return { storeIds: ids, skipped: false };
663
+ }
664
+ const detailed = await Promise.all(
665
+ ids.map(async (id) => {
666
+ const result = await client.validateStore(id);
667
+ return {
668
+ id,
669
+ name: result.resource?.name ?? "(unnamed)",
670
+ slug: result.resource?.slug ?? ""
671
+ };
672
+ })
673
+ );
674
+ const picked = await pickStores(detailed);
675
+ if (picked.length === 0) {
676
+ throw new FreshSqueezyError({
677
+ code: "NO_SELECTION",
678
+ message: "No store selected. Pick at least one to continue."
679
+ });
680
+ }
681
+ return { storeIds: picked, skipped: false };
682
+ }
683
+ async function fetchReachableStoreIds(client) {
684
+ const connection = await client.validateConnection();
685
+ if (!connection.ok) {
686
+ const authIssue = connection.issues.find((entry) => entry.code === "AUTH_FAILED");
687
+ if (authIssue) {
688
+ throw new FreshSqueezyError({
689
+ code: "AUTH_FAILED",
690
+ message: authIssue.message
691
+ });
692
+ }
693
+ }
694
+ return connection.resource?.storeIds ?? [];
695
+ }
696
+ function dedupe(values) {
697
+ return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
698
+ }
699
+
700
+ // src/cli/commands/doctor.ts
701
+ async function runDoctorCommand(options) {
702
+ try {
703
+ const client = createFreshSqueezy({ mode: options.mode });
704
+ const resolved = await resolveStores(client, {
705
+ storeIds: options.storeIds,
706
+ allStores: options.allStores,
707
+ isInteractive: options.isInteractive ?? false
708
+ });
709
+ if (resolved.skipped) {
710
+ return await runConnectionOnly(client, options);
711
+ }
712
+ const reports = await Promise.all(
713
+ resolved.storeIds.map(
714
+ (storeId) => client.doctor({
715
+ storeId,
716
+ productId: options.productId,
717
+ webhookUrl: options.webhookUrl
718
+ })
719
+ )
720
+ );
721
+ const ok = reports.every((report) => report.ok);
722
+ const payload = { ok, mode: client.mode, reports };
723
+ if (options.json) {
724
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
725
+ `);
726
+ } else {
727
+ for (const report of reports) {
728
+ process.stdout.write(`${renderReport(report)}
729
+
730
+ `);
731
+ }
732
+ }
733
+ return ok ? 0 : 1;
734
+ } catch (err) {
735
+ writeFatal(err, options.json ?? false);
736
+ return 2;
737
+ }
738
+ }
739
+ async function runConnectionOnly(client, options) {
740
+ const connection = await client.validateConnection();
741
+ const report = {
742
+ ok: connection.ok,
743
+ mode: client.mode,
744
+ results: [connection]
745
+ };
746
+ const payload = { ok: connection.ok, mode: client.mode, reports: [report] };
747
+ if (options.json) {
748
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
749
+ `);
750
+ } else {
751
+ process.stderr.write(
752
+ "fresh-squeezy: no --store-ids or --all-stores and stdin is not a TTY; running connection-only.\n"
753
+ );
754
+ process.stdout.write(`${renderReport(report)}
755
+ `);
756
+ }
757
+ return connection.ok ? 0 : 1;
758
+ }
759
+ function writeFatal(err, asJson) {
760
+ if (asJson) {
761
+ const payload = err instanceof FreshSqueezyError ? { ok: false, error: { code: err.code, message: err.message, status: err.status ?? null } } : { ok: false, error: { code: "UNKNOWN", message: err instanceof Error ? err.message : String(err) } };
762
+ process.stderr.write(`${JSON.stringify(payload)}
763
+ `);
764
+ return;
765
+ }
766
+ const message = err instanceof Error ? err.message : String(err);
767
+ process.stderr.write(`fresh-squeezy: ${message}
768
+ `);
769
+ }
770
+
771
+ // src/cli/commands/validate.ts
772
+ async function runValidateCommand(target, options) {
773
+ try {
774
+ const client = createFreshSqueezy({ mode: options.mode });
775
+ if (target === "connection") {
776
+ return emit(await client.validateConnection(), options);
777
+ }
778
+ if (target === "product") {
779
+ return emit(await runProduct(client, options), options);
780
+ }
781
+ const storeIds = await resolveStoresForTarget(client, target, options);
782
+ const results = await runPerStore(client, target, storeIds, options);
783
+ if (options.json) {
784
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
785
+ `);
786
+ } else {
787
+ for (const result of results) {
788
+ process.stdout.write(`${renderResult(result)}
789
+
790
+ `);
791
+ }
792
+ }
793
+ return results.every((result) => result.ok) ? 0 : 1;
794
+ } catch (err) {
795
+ const message = err instanceof FreshSqueezyError ? err.message : err instanceof Error ? err.message : String(err);
796
+ process.stderr.write(`fresh-squeezy: ${message}
797
+ `);
798
+ return 2;
799
+ }
800
+ }
801
+ async function resolveStoresForTarget(client, target, options) {
802
+ const resolved = await resolveStores(client, {
803
+ storeIds: options.storeIds,
804
+ allStores: options.allStores,
805
+ isInteractive: options.isInteractive ?? false
806
+ });
807
+ if (resolved.skipped) {
808
+ throw new FreshSqueezyError({
809
+ code: "MISSING_ARG",
810
+ message: `--store-ids or --all-stores is required for \`validate ${target}\` in non-interactive mode.`
811
+ });
812
+ }
813
+ return resolved.storeIds;
814
+ }
815
+ async function runPerStore(client, target, storeIds, options) {
816
+ if (target === "store") {
817
+ return Promise.all(storeIds.map((id) => client.validateStore(id)));
818
+ }
819
+ if (target === "webhook") {
820
+ const url = required(options.webhookUrl, "--webhook-url is required for `validate webhook`.");
821
+ return Promise.all(
822
+ storeIds.map((storeId) => client.validateWebhook({ storeId, url }))
823
+ );
824
+ }
825
+ return [];
826
+ }
827
+ async function runProduct(client, options) {
828
+ const productId = required(options.productId, "--product-id is required for `validate product`.");
829
+ const expected = options.storeIds?.[0];
830
+ return client.validateProduct({
831
+ productId,
832
+ expectedStoreId: expected
833
+ });
834
+ }
835
+ function emit(result, options) {
836
+ if (options.json) {
837
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
838
+ `);
839
+ } else {
840
+ process.stdout.write(`${renderResult(result)}
841
+ `);
842
+ }
843
+ return result.ok ? 0 : 1;
844
+ }
845
+ function required(value, message) {
846
+ if (value === void 0 || value === null || value === "") {
847
+ throw new FreshSqueezyError({ code: "MISSING_ARG", message });
848
+ }
849
+ return value;
850
+ }
851
+
852
+ // src/cli/commands/init.ts
853
+ import fs from "fs/promises";
854
+ import path from "path";
855
+ import chalk2 from "chalk";
856
+ async function runInitCommand(options = {}) {
857
+ const answers = await askForCredentials();
858
+ const client = createFreshSqueezy({ apiKey: answers.apiKey, mode: answers.mode });
859
+ const connection = await client.validateConnection();
860
+ if (!connection.ok) {
861
+ process.stdout.write(`${renderReport({ ok: false, mode: answers.mode, results: [connection] })}
862
+ `);
863
+ return 1;
864
+ }
865
+ const storeIds = connection.resource?.storeIds ?? [];
866
+ if (storeIds.length === 0) {
867
+ process.stdout.write(
868
+ chalk2.yellow("No stores reachable with this key. Create a store in Lemon Squeezy and retry.\n")
869
+ );
870
+ return 1;
871
+ }
872
+ const stores = await Promise.all(storeIds.map((id) => client.validateStore(id)));
873
+ const pickable = stores.filter((entry) => entry.ok && entry.resource).map((entry, index) => ({
874
+ id: storeIds[index] ?? "",
875
+ name: entry.resource?.name ?? "(unnamed)",
876
+ slug: entry.resource?.slug ?? ""
877
+ })).filter((entry) => entry.id !== "");
878
+ const storeId = await pickStore(pickable);
879
+ const envPath = path.resolve(process.cwd(), options.envFile ?? ".env.local");
880
+ const shouldWrite = await confirmWriteEnvFile(envPath);
881
+ if (shouldWrite) {
882
+ await writeEnvFile(envPath, { apiKey: answers.apiKey, mode: answers.mode, storeId });
883
+ process.stdout.write(chalk2.green(`Wrote ${envPath}
884
+ `));
885
+ }
886
+ process.stdout.write(chalk2.dim("\nRunning doctor...\n\n"));
887
+ const report = await client.doctor({ storeId });
888
+ process.stdout.write(`${renderReport(report)}
889
+ `);
890
+ return report.ok ? 0 : 1;
891
+ }
892
+ async function writeEnvFile(envPath, values) {
893
+ const lines = [
894
+ `${ENV_KEYS.apiKey}=${values.apiKey}`,
895
+ `${ENV_KEYS.storeId}=${values.storeId}`,
896
+ `${ENV_KEYS.mode}=${values.mode}`,
897
+ ""
898
+ ];
899
+ await fs.writeFile(envPath, lines.join("\n"), { encoding: "utf8" });
900
+ }
901
+
902
+ // src/cli/main.ts
903
+ dotenv.config({ path: ".env.local" });
904
+ dotenv.config();
905
+ var isInteractive = Boolean(process.stdin.isTTY);
906
+ var program = new Command();
907
+ program.name("fresh-squeezy").description("Validator-first Lemon Squeezy setup doctor").version("0.1.0");
908
+ program.command("doctor").description("Run every configured validator and emit a report").option("-m, --mode <mode>", "test or live", parseMode).option("--store-ids <ids>", "Comma-separated store IDs (e.g. 1,2,3)", parseCsv).option("--all-stores", "Run against every reachable store, no prompt").option("--product-id <id>", "Product to validate").option("--webhook-url <url>", "Webhook URL to validate").option("--json", "Emit machine-readable JSON").action(async (opts) => {
909
+ const code = await runDoctorCommand({
910
+ mode: opts.mode,
911
+ storeIds: opts.storeIds,
912
+ allStores: Boolean(opts.allStores),
913
+ productId: opts.productId,
914
+ webhookUrl: opts.webhookUrl,
915
+ json: Boolean(opts.json),
916
+ isInteractive
917
+ });
918
+ process.exit(code);
919
+ });
920
+ var validate = program.command("validate").description("Run a single validator");
921
+ validate.command("connection").description("Check that the API key authenticates").option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("connection", opts));
922
+ validate.command("store").description("Check one or more stores are reachable").option("--store-ids <ids>", "Comma-separated store IDs", parseCsv).option("--all-stores", "Run against every reachable store").option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("store", opts));
923
+ validate.command("product").description("Check a product is published with at least one variant").requiredOption("--product-id <id>", "Product ID to validate").option("--store-ids <ids>", "Expected owning store IDs (first is used for cross-check)", parseCsv).option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("product", opts));
924
+ validate.command("webhook").description("Check a webhook is registered with the recommended events").requiredOption("--webhook-url <url>", "Public webhook URL").option("--store-ids <ids>", "Comma-separated store IDs", parseCsv).option("--all-stores", "Run against every reachable store").option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("webhook", opts));
925
+ program.command("init").description("Interactive setup: ask for credentials, pick a store, run doctor").option("--env-file <path>", "Where to write credentials (default: .env.local)").action(async (opts) => {
926
+ const code = await runInitCommand({ envFile: opts.envFile });
927
+ process.exit(code);
928
+ });
929
+ program.parseAsync(process.argv).catch((err) => {
930
+ const message = err instanceof Error ? err.message : String(err);
931
+ process.stderr.write(`fresh-squeezy: ${message}
932
+ `);
933
+ process.exit(2);
934
+ });
935
+ function parseMode(value) {
936
+ if (value === "test" || value === "live") return value;
937
+ throw new Error(`Mode must be "test" or "live", got "${value}"`);
938
+ }
939
+ function parseCsv(value) {
940
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
941
+ }
942
+ async function runValidate(target, opts) {
943
+ const code = await runValidateCommand(target, {
944
+ mode: opts.mode,
945
+ storeIds: opts.storeIds,
946
+ allStores: Boolean(opts.allStores),
947
+ productId: opts.productId,
948
+ webhookUrl: opts.webhookUrl,
949
+ json: Boolean(opts.json),
950
+ isInteractive
951
+ });
952
+ process.exit(code);
953
+ }
954
+ //# sourceMappingURL=cli.js.map