fresh-squeezy 0.1.11 → 0.1.13

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 CHANGED
@@ -74,7 +74,7 @@ function renderAugmentationFile(flavour) {
74
74
 
75
75
  `;
76
76
  if (flavour === "with-sdk") {
77
- return header + `import type {
77
+ return `${header}import type {
78
78
  Subscription,
79
79
  SubscriptionInvoice,
80
80
  Order,
@@ -165,7 +165,7 @@ type AugmentResponse<T, Fields> = T extends {
165
165
  */
166
166
  `;
167
167
  }
168
- return header + `import type {
168
+ return `${header}import type {
169
169
  WithLatestLemonSqueezyFields,
170
170
  LatestSubscriptionFields,
171
171
  LatestSubscriptionInvoiceFields,
@@ -239,22 +239,22 @@ var ENV_KEYS = {
239
239
  storeId: "LEMON_SQUEEZY_STORE_ID",
240
240
  mode: "LEMON_SQUEEZY_MODE"
241
241
  };
242
- function resolveConfig(input = {}) {
243
- const apiKey = input.apiKey ?? process.env[ENV_KEYS.apiKey];
242
+ function resolveConfig(input2 = {}) {
243
+ const apiKey = input2.apiKey ?? process.env[ENV_KEYS.apiKey];
244
244
  if (!apiKey) {
245
245
  throw new FreshSqueezyError({
246
246
  code: "MISSING_API_KEY",
247
247
  message: `No API key provided. Pass \`apiKey\` or set ${ENV_KEYS.apiKey}.`
248
248
  });
249
249
  }
250
- const mode = normalizeMode(input.mode ?? process.env[ENV_KEYS.mode] ?? "test");
251
- const storeIdRaw = input.storeId ?? process.env[ENV_KEYS.storeId];
250
+ const mode = normalizeMode(input2.mode ?? process.env[ENV_KEYS.mode] ?? "test");
251
+ const storeIdRaw = input2.storeId ?? process.env[ENV_KEYS.storeId];
252
252
  return {
253
253
  apiKey,
254
254
  storeId: storeIdRaw == null ? void 0 : String(storeIdRaw),
255
255
  mode,
256
- baseUrl: input.baseUrl ?? DEFAULT_BASE_URL,
257
- fetch: input.fetch ?? globalThis.fetch
256
+ baseUrl: input2.baseUrl ?? DEFAULT_BASE_URL,
257
+ fetch: input2.fetch ?? globalThis.fetch
258
258
  };
259
259
  }
260
260
  function normalizeMode(value) {
@@ -379,11 +379,6 @@ function extractJsonApiErrors(body) {
379
379
  );
380
380
  }
381
381
 
382
- // src/resources/users.ts
383
- async function getAuthenticatedUser(http) {
384
- return http.request({ path: "/v1/users/me" });
385
- }
386
-
387
382
  // src/core/mode.ts
388
383
  function resolveActualMode(testMode) {
389
384
  if (testMode === true) return "test";
@@ -399,6 +394,11 @@ async function listStores(http) {
399
394
  return http.paginate("/v1/stores");
400
395
  }
401
396
 
397
+ // src/resources/users.ts
398
+ async function getAuthenticatedUser(http) {
399
+ return http.request({ path: "/v1/users/me" });
400
+ }
401
+
402
402
  // src/validate/rules.ts
403
403
  var ISSUE_CODES = {
404
404
  AUTH_FAILED: "AUTH_FAILED",
@@ -487,20 +487,20 @@ function mapErrorToIssue(err, mapping = {}) {
487
487
  return issue(ISSUE_CODES.UNKNOWN, "error", message);
488
488
  }
489
489
  async function probeFetch(fetcher, options) {
490
+ return probeCollection(fetcher, {
491
+ notFound: {
492
+ code: options.notFoundCode,
493
+ message: options.notFoundMessage,
494
+ ...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
495
+ ...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
496
+ }
497
+ });
498
+ }
499
+ async function probeCollection(fetcher, mapping = {}) {
490
500
  try {
491
501
  return { ok: true, resource: await fetcher() };
492
502
  } catch (err) {
493
- return {
494
- ok: false,
495
- issue: mapErrorToIssue(err, {
496
- notFound: {
497
- code: options.notFoundCode,
498
- message: options.notFoundMessage,
499
- ...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
500
- ...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
501
- }
502
- })
503
- };
503
+ return { ok: false, issue: mapErrorToIssue(err, mapping) };
504
504
  }
505
505
  }
506
506
  function checkStoreOwnership(check) {
@@ -526,281 +526,57 @@ function checkStoreOwnership(check) {
526
526
  // src/validate/connection.ts
527
527
  async function validateConnection(http, mode) {
528
528
  const issues = [];
529
- try {
530
- const userDoc = await getAuthenticatedUser(http);
531
- const stores = await listStores(http);
532
- const actualMode = resolveActualMode(userDoc.meta?.test_mode);
533
- const summary = {
534
- user: userDoc.data.attributes,
535
- storeCount: stores.length,
536
- storeIds: stores.map((store) => store.id),
537
- declaredMode: mode,
538
- ...actualMode ? { actualMode } : {}
539
- };
540
- if (actualMode && actualMode !== mode) {
541
- issues.push(
542
- issue(
543
- ISSUE_CODES.MODE_MISMATCH,
544
- "error",
545
- `API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
546
- {
547
- suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
548
- context: { declared: mode, actual: actualMode }
549
- }
550
- )
551
- );
552
- }
553
- if (stores.length === 0) {
554
- issues.push(
555
- issue(
556
- ISSUE_CODES.STORE_NOT_FOUND,
557
- "warning",
558
- "API key authenticated but no stores are reachable.",
559
- { suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
560
- )
561
- );
529
+ const fetched = await probeCollection(
530
+ async () => {
531
+ const userDoc2 = await getAuthenticatedUser(http);
532
+ const stores2 = await listStores(http);
533
+ return { userDoc: userDoc2, stores: stores2 };
534
+ },
535
+ {
536
+ unauthorized: {
537
+ code: ISSUE_CODES.AUTH_FAILED,
538
+ message: "API key rejected by Lemon Squeezy.",
539
+ suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
540
+ }
562
541
  }
563
- return buildResult("connection", mode, issues, summary);
564
- } catch (err) {
565
- issues.push(
566
- mapErrorToIssue(err, {
567
- unauthorized: {
568
- code: ISSUE_CODES.AUTH_FAILED,
569
- message: "API key rejected by Lemon Squeezy.",
570
- suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
571
- }
572
- })
573
- );
574
- return buildResult("connection", mode, issues);
575
- }
576
- }
577
-
578
- // src/validate/store.ts
579
- async function validateStore(http, mode, storeId) {
580
- const issues = [];
581
- const fetched = await probeFetch(() => getStore(http, storeId), {
582
- notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
583
- notFoundMessage: `Store ${storeId} not found with the current API key.`,
584
- notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
585
- notFoundContext: { storeId: String(storeId) }
586
- });
587
- if (!fetched.ok) {
588
- issues.push(fetched.issue);
589
- return buildResult("store", mode, issues, void 0, {
590
- label: `store ${storeId}`,
591
- id: String(storeId)
592
- });
593
- }
594
- return buildResult("store", mode, issues, fetched.resource.attributes, {
595
- label: fetched.resource.attributes.name,
596
- id: String(storeId)
597
- });
598
- }
599
-
600
- // src/resources/products.ts
601
- async function getProduct(http, productId) {
602
- return http.getResource(`/v1/products/${productId}`);
603
- }
604
-
605
- // src/resources/variants.ts
606
- async function getVariant(http, variantId) {
607
- return http.getResource(`/v1/variants/${variantId}`);
608
- }
609
- async function listVariantsForProduct(http, productId) {
610
- return http.paginate("/v1/variants", {
611
- "filter[product_id]": String(productId)
612
- });
613
- }
614
-
615
- // src/validate/product.ts
616
- async function validateProduct(http, mode, options) {
617
- const issues = [];
618
- const fetched = await probeFetch(() => getProduct(http, options.productId), {
619
- notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
620
- notFoundMessage: `Product ${options.productId} not found.`,
621
- notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
622
- notFoundContext: { productId: String(options.productId) }
623
- });
542
+ );
624
543
  if (!fetched.ok) {
625
544
  issues.push(fetched.issue);
626
- return buildResult("product", mode, issues, void 0, {
627
- label: `product ${options.productId}`,
628
- id: String(options.productId)
629
- });
630
- }
631
- const attrs = fetched.resource.attributes;
632
- if (options.expectedStoreId !== void 0) {
633
- const mismatch = checkStoreOwnership({
634
- expectedStoreId: options.expectedStoreId,
635
- actualStoreId: attrs.store_id,
636
- code: ISSUE_CODES.PRODUCT_WRONG_STORE,
637
- label: "Product",
638
- suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
639
- });
640
- if (mismatch) issues.push(mismatch);
545
+ return buildResult("connection", mode, issues);
641
546
  }
642
- if (attrs.status !== "published") {
547
+ const { userDoc, stores } = fetched.resource;
548
+ const actualMode = resolveActualMode(userDoc.meta?.test_mode);
549
+ const summary = {
550
+ user: userDoc.data.attributes,
551
+ storeCount: stores.length,
552
+ storeIds: stores.map((store) => store.id),
553
+ declaredMode: mode,
554
+ ...actualMode ? { actualMode } : {}
555
+ };
556
+ if (actualMode && actualMode !== mode) {
643
557
  issues.push(
644
558
  issue(
645
- ISSUE_CODES.PRODUCT_UNPUBLISHED,
559
+ ISSUE_CODES.MODE_MISMATCH,
646
560
  "error",
647
- `Product is in "${attrs.status}" state, not "published".`,
561
+ `API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
648
562
  {
649
- suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
650
- context: { status: attrs.status }
563
+ suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
564
+ context: { declared: mode, actual: actualMode }
651
565
  }
652
566
  )
653
567
  );
654
568
  }
655
- if (!attrs.buy_now_url) {
569
+ if (stores.length === 0) {
656
570
  issues.push(
657
571
  issue(
658
- ISSUE_CODES.PRODUCT_NO_BUY_URL,
572
+ ISSUE_CODES.STORE_NOT_FOUND,
659
573
  "warning",
660
- "Product has no buy-now URL. Hosted checkout may be disabled.",
661
- { suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
574
+ "API key authenticated but no stores are reachable.",
575
+ { suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
662
576
  )
663
577
  );
664
578
  }
665
- try {
666
- const variants = await listVariantsForProduct(http, options.productId);
667
- if (variants.length === 0) {
668
- issues.push(
669
- issue(
670
- ISSUE_CODES.VARIANT_MISSING,
671
- "error",
672
- "Product has no variants. Customers cannot purchase it.",
673
- { suggestedFix: "Add at least one variant in the product configuration." }
674
- )
675
- );
676
- } else if (!variants.some((variant) => variant.attributes.status === "published")) {
677
- issues.push(
678
- issue(
679
- ISSUE_CODES.VARIANT_UNPUBLISHED,
680
- "error",
681
- "Product has variants but none are published.",
682
- { suggestedFix: "Publish at least one variant." }
683
- )
684
- );
685
- }
686
- } catch (err) {
687
- const message = err instanceof Error ? err.message : "Unknown error fetching variants";
688
- issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
689
- }
690
- return buildResult("product", mode, issues, attrs, {
691
- label: attrs.name,
692
- id: String(options.productId)
693
- });
694
- }
695
-
696
- // src/core/equality.ts
697
- function sameWebhookUrl(a, b) {
698
- return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
699
- }
700
- function normalizeWebhookUrl(raw) {
701
- return raw.replace(/\/+$/, "").toLowerCase();
702
- }
703
-
704
- // src/resources/webhooks.ts
705
- async function listWebhooksForStore(http, storeId) {
706
- return http.paginate("/v1/webhooks", {
707
- "filter[store_id]": String(storeId)
708
- });
709
- }
710
-
711
- // src/support/manifest.ts
712
- var RECOMMENDED_WEBHOOK_EVENTS = [
713
- "order_created",
714
- "order_refunded",
715
- "subscription_created",
716
- "subscription_updated",
717
- "subscription_cancelled",
718
- "subscription_resumed",
719
- "subscription_expired",
720
- "subscription_payment_success",
721
- "subscription_payment_failed"
722
- ];
723
- var OPTIONAL_WEBHOOK_EVENTS = [
724
- "customer_updated",
725
- "affiliate_activated",
726
- "license_key_created",
727
- "license_key_updated"
728
- ];
729
-
730
- // src/validate/webhook.ts
731
- async function validateWebhook(http, mode, options) {
732
- const issues = [];
733
- let webhooks;
734
- try {
735
- webhooks = await listWebhooksForStore(http, options.storeId);
736
- } catch (err) {
737
- if (err instanceof FreshSqueezyError) {
738
- issues.push(
739
- issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
740
- context: { status: err.status ?? null, code: err.code }
741
- })
742
- );
743
- return buildResult("webhook", mode, issues, void 0, {
744
- label: options.url,
745
- url: options.url
746
- });
747
- }
748
- const message = err instanceof Error ? err.message : "Unknown error";
749
- issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
750
- return buildResult("webhook", mode, issues, void 0, {
751
- label: options.url,
752
- url: options.url
753
- });
754
- }
755
- const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
756
- if (!match) {
757
- issues.push(
758
- issue(
759
- ISSUE_CODES.WEBHOOK_NOT_FOUND,
760
- "error",
761
- `No webhook registered for URL ${options.url} on store ${options.storeId}.`,
762
- {
763
- suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
764
- context: { storeId: String(options.storeId), url: options.url }
765
- }
766
- )
767
- );
768
- return buildResult("webhook", mode, issues, void 0, {
769
- label: options.url,
770
- url: options.url
771
- });
772
- }
773
- const subscribed = new Set(match.attributes.events);
774
- const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
775
- const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
776
- if (missingRecommended.length > 0) {
777
- issues.push(
778
- issue(
779
- ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
780
- "error",
781
- `Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
782
- {
783
- suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
784
- context: { missing: missingRecommended.join(",") }
785
- }
786
- )
787
- );
788
- }
789
- if (missingOptional.length > 0) {
790
- issues.push(
791
- issue(
792
- ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
793
- "info",
794
- `Optional events not subscribed: ${missingOptional.join(", ")}.`,
795
- { context: { missing: missingOptional.join(",") } }
796
- )
797
- );
798
- }
799
- return buildResult("webhook", mode, issues, match.attributes, {
800
- label: match.attributes.url,
801
- id: match.id,
802
- url: match.attributes.url
803
- });
579
+ return buildResult("connection", mode, issues, summary);
804
580
  }
805
581
 
806
582
  // src/resources/discounts.ts
@@ -825,6 +601,14 @@ async function validateDiscount(http, mode, options) {
825
601
  });
826
602
  }
827
603
  const attrs = fetched.resource.attributes;
604
+ issues.push(...checkDiscount(attrs, { storeId: options.storeId }));
605
+ return buildResult("discount", mode, issues, attrs, {
606
+ label: `${attrs.name} (${attrs.code})`,
607
+ id: String(options.discountId)
608
+ });
609
+ }
610
+ function checkDiscount(attrs, options) {
611
+ const issues = [];
828
612
  const mismatch = checkStoreOwnership({
829
613
  expectedStoreId: options.storeId,
830
614
  actualStoreId: attrs.store_id,
@@ -911,10 +695,7 @@ async function validateDiscount(http, mode, options) {
911
695
  )
912
696
  );
913
697
  }
914
- return buildResult("discount", mode, issues, attrs, {
915
- label: `${attrs.name} (${attrs.code})`,
916
- id: String(options.discountId)
917
- });
698
+ return issues;
918
699
  }
919
700
 
920
701
  // src/resources/licenseKeys.ts
@@ -939,6 +720,14 @@ async function validateLicenseKey(http, mode, options) {
939
720
  });
940
721
  }
941
722
  const attrs = fetched.resource.attributes;
723
+ issues.push(...checkLicenseKey(attrs, { storeId: options.storeId }));
724
+ return buildResult("licenseKey", mode, issues, attrs, {
725
+ label: attrs.key_short,
726
+ id: String(options.licenseKeyId)
727
+ });
728
+ }
729
+ function checkLicenseKey(attrs, options) {
730
+ const issues = [];
942
731
  const mismatch = checkStoreOwnership({
943
732
  expectedStoreId: options.storeId,
944
733
  actualStoreId: attrs.store_id,
@@ -990,9 +779,129 @@ async function validateLicenseKey(http, mode, options) {
990
779
  )
991
780
  );
992
781
  }
993
- return buildResult("licenseKey", mode, issues, attrs, {
994
- label: attrs.key_short,
995
- id: String(options.licenseKeyId)
782
+ return issues;
783
+ }
784
+
785
+ // src/resources/products.ts
786
+ async function getProduct(http, productId) {
787
+ return http.getResource(`/v1/products/${productId}`);
788
+ }
789
+
790
+ // src/resources/variants.ts
791
+ async function getVariant(http, variantId) {
792
+ return http.getResource(`/v1/variants/${variantId}`);
793
+ }
794
+ async function listVariantsForProduct(http, productId) {
795
+ return http.paginate("/v1/variants", {
796
+ "filter[product_id]": String(productId)
797
+ });
798
+ }
799
+
800
+ // src/validate/product.ts
801
+ async function validateProduct(http, mode, options) {
802
+ const issues = [];
803
+ const fetched = await probeFetch(() => getProduct(http, options.productId), {
804
+ notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
805
+ notFoundMessage: `Product ${options.productId} not found.`,
806
+ notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
807
+ notFoundContext: { productId: String(options.productId) }
808
+ });
809
+ if (!fetched.ok) {
810
+ issues.push(fetched.issue);
811
+ return buildResult("product", mode, issues, void 0, {
812
+ label: `product ${options.productId}`,
813
+ id: String(options.productId)
814
+ });
815
+ }
816
+ const attrs = fetched.resource.attributes;
817
+ issues.push(...checkProduct(attrs, { expectedStoreId: options.expectedStoreId }));
818
+ try {
819
+ const variants = await listVariantsForProduct(http, options.productId);
820
+ if (variants.length === 0) {
821
+ issues.push(
822
+ issue(
823
+ ISSUE_CODES.VARIANT_MISSING,
824
+ "error",
825
+ "Product has no variants. Customers cannot purchase it.",
826
+ { suggestedFix: "Add at least one variant in the product configuration." }
827
+ )
828
+ );
829
+ } else if (!variants.some((variant) => variant.attributes.status === "published")) {
830
+ issues.push(
831
+ issue(
832
+ ISSUE_CODES.VARIANT_UNPUBLISHED,
833
+ "error",
834
+ "Product has variants but none are published.",
835
+ { suggestedFix: "Publish at least one variant." }
836
+ )
837
+ );
838
+ }
839
+ } catch (err) {
840
+ const message = err instanceof Error ? err.message : "Unknown error fetching variants";
841
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
842
+ }
843
+ return buildResult("product", mode, issues, attrs, {
844
+ label: attrs.name,
845
+ id: String(options.productId)
846
+ });
847
+ }
848
+ function checkProduct(attrs, options = {}) {
849
+ const issues = [];
850
+ if (options.expectedStoreId !== void 0) {
851
+ const mismatch = checkStoreOwnership({
852
+ expectedStoreId: options.expectedStoreId,
853
+ actualStoreId: attrs.store_id,
854
+ code: ISSUE_CODES.PRODUCT_WRONG_STORE,
855
+ label: "Product",
856
+ suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
857
+ });
858
+ if (mismatch) issues.push(mismatch);
859
+ }
860
+ if (attrs.status !== "published") {
861
+ issues.push(
862
+ issue(
863
+ ISSUE_CODES.PRODUCT_UNPUBLISHED,
864
+ "error",
865
+ `Product is in "${attrs.status}" state, not "published".`,
866
+ {
867
+ suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
868
+ context: { status: attrs.status }
869
+ }
870
+ )
871
+ );
872
+ }
873
+ if (!attrs.buy_now_url) {
874
+ issues.push(
875
+ issue(
876
+ ISSUE_CODES.PRODUCT_NO_BUY_URL,
877
+ "warning",
878
+ "Product has no buy-now URL. Hosted checkout may be disabled.",
879
+ { suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
880
+ )
881
+ );
882
+ }
883
+ return issues;
884
+ }
885
+
886
+ // src/validate/store.ts
887
+ async function validateStore(http, mode, storeId) {
888
+ const issues = [];
889
+ const fetched = await probeFetch(() => getStore(http, storeId), {
890
+ notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
891
+ notFoundMessage: `Store ${storeId} not found with the current API key.`,
892
+ notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
893
+ notFoundContext: { storeId: String(storeId) }
894
+ });
895
+ if (!fetched.ok) {
896
+ issues.push(fetched.issue);
897
+ return buildResult("store", mode, issues, void 0, {
898
+ label: `store ${storeId}`,
899
+ id: String(storeId)
900
+ });
901
+ }
902
+ return buildResult("store", mode, issues, fetched.resource.attributes, {
903
+ label: fetched.resource.attributes.name,
904
+ id: String(storeId)
996
905
  });
997
906
  }
998
907
 
@@ -1018,6 +927,35 @@ async function validateSubscriptionPlan(http, mode, options) {
1018
927
  }
1019
928
  const variant = fetched.resource;
1020
929
  const attrs = variant.attributes;
930
+ issues.push(...checkSubscriptionPlan(attrs, { variantId: options.variantId }));
931
+ try {
932
+ const product = await getProduct(http, attrs.product_id);
933
+ const mismatch = checkStoreOwnership({
934
+ expectedStoreId: options.storeId,
935
+ actualStoreId: product.attributes.store_id,
936
+ code: ISSUE_CODES.PLAN_STORE_MISMATCH,
937
+ label: `Subscription variant (via product ${attrs.product_id})`,
938
+ suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
939
+ extraContext: { productId: String(attrs.product_id) }
940
+ });
941
+ if (mismatch) issues.push(mismatch);
942
+ } catch {
943
+ }
944
+ const summary = {
945
+ variantId: variant.id,
946
+ interval: attrs.interval,
947
+ intervalCount: attrs.interval_count,
948
+ price: attrs.price,
949
+ hasFreeTrial: attrs.has_free_trial,
950
+ status: attrs.status
951
+ };
952
+ return buildResult("subscriptionPlan", mode, issues, summary, {
953
+ label: attrs.name,
954
+ id: String(options.variantId)
955
+ });
956
+ }
957
+ function checkSubscriptionPlan(attrs, options) {
958
+ const issues = [];
1021
959
  if (!attrs.is_subscription) {
1022
960
  issues.push(
1023
961
  issue(
@@ -1062,7 +1000,7 @@ async function validateSubscriptionPlan(http, mode, options) {
1062
1000
  issue(
1063
1001
  ISSUE_CODES.PLAN_FREE_PRICE,
1064
1002
  "warning",
1065
- `Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.`,
1003
+ "Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.",
1066
1004
  {
1067
1005
  suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
1068
1006
  context: { price: attrs.price }
@@ -1091,7 +1029,7 @@ async function validateSubscriptionPlan(http, mode, options) {
1091
1029
  issue(
1092
1030
  ISSUE_CODES.PLAN_DRAFT,
1093
1031
  "warning",
1094
- `Subscription variant is in draft status \u2014 customers cannot subscribe.`,
1032
+ "Subscription variant is in draft status \u2014 customers cannot subscribe.",
1095
1033
  {
1096
1034
  suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
1097
1035
  context: { status: attrs.status }
@@ -1099,30 +1037,103 @@ async function validateSubscriptionPlan(http, mode, options) {
1099
1037
  )
1100
1038
  );
1101
1039
  }
1102
- try {
1103
- const product = await getProduct(http, attrs.product_id);
1104
- const mismatch = checkStoreOwnership({
1105
- expectedStoreId: options.storeId,
1106
- actualStoreId: product.attributes.store_id,
1107
- code: ISSUE_CODES.PLAN_STORE_MISMATCH,
1108
- label: `Subscription variant (via product ${attrs.product_id})`,
1109
- suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
1110
- extraContext: { productId: String(attrs.product_id) }
1040
+ return issues;
1041
+ }
1042
+
1043
+ // src/core/equality.ts
1044
+ function sameWebhookUrl(a, b) {
1045
+ return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
1046
+ }
1047
+ function normalizeWebhookUrl(raw) {
1048
+ return raw.replace(/\/+$/, "").toLowerCase();
1049
+ }
1050
+
1051
+ // src/resources/webhooks.ts
1052
+ async function listWebhooksForStore(http, storeId) {
1053
+ return http.paginate("/v1/webhooks", {
1054
+ "filter[store_id]": String(storeId)
1055
+ });
1056
+ }
1057
+
1058
+ // src/support/manifest.ts
1059
+ var RECOMMENDED_WEBHOOK_EVENTS = [
1060
+ "order_created",
1061
+ "order_refunded",
1062
+ "subscription_created",
1063
+ "subscription_updated",
1064
+ "subscription_cancelled",
1065
+ "subscription_resumed",
1066
+ "subscription_expired",
1067
+ "subscription_payment_success",
1068
+ "subscription_payment_failed"
1069
+ ];
1070
+ var OPTIONAL_WEBHOOK_EVENTS = [
1071
+ "customer_updated",
1072
+ "affiliate_activated",
1073
+ "license_key_created",
1074
+ "license_key_updated"
1075
+ ];
1076
+
1077
+ // src/validate/webhook.ts
1078
+ async function validateWebhook(http, mode, options) {
1079
+ const issues = [];
1080
+ const fetched = await probeCollection(() => listWebhooksForStore(http, options.storeId));
1081
+ if (!fetched.ok) {
1082
+ issues.push(fetched.issue);
1083
+ return buildResult("webhook", mode, issues, void 0, {
1084
+ label: options.url,
1085
+ url: options.url
1111
1086
  });
1112
- if (mismatch) issues.push(mismatch);
1113
- } catch {
1114
1087
  }
1115
- const summary = {
1116
- variantId: variant.id,
1117
- interval: attrs.interval,
1118
- intervalCount: attrs.interval_count,
1119
- price: attrs.price,
1120
- hasFreeTrial: attrs.has_free_trial,
1121
- status: attrs.status
1122
- };
1123
- return buildResult("subscriptionPlan", mode, issues, summary, {
1124
- label: attrs.name,
1125
- id: String(options.variantId)
1088
+ const webhooks = fetched.resource;
1089
+ const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
1090
+ if (!match) {
1091
+ issues.push(
1092
+ issue(
1093
+ ISSUE_CODES.WEBHOOK_NOT_FOUND,
1094
+ "error",
1095
+ `No webhook registered for URL ${options.url} on store ${options.storeId}.`,
1096
+ {
1097
+ suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
1098
+ context: { storeId: String(options.storeId), url: options.url }
1099
+ }
1100
+ )
1101
+ );
1102
+ return buildResult("webhook", mode, issues, void 0, {
1103
+ label: options.url,
1104
+ url: options.url
1105
+ });
1106
+ }
1107
+ const subscribed = new Set(match.attributes.events);
1108
+ const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
1109
+ const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
1110
+ if (missingRecommended.length > 0) {
1111
+ issues.push(
1112
+ issue(
1113
+ ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
1114
+ "error",
1115
+ `Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
1116
+ {
1117
+ suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
1118
+ context: { missing: missingRecommended.join(",") }
1119
+ }
1120
+ )
1121
+ );
1122
+ }
1123
+ if (missingOptional.length > 0) {
1124
+ issues.push(
1125
+ issue(
1126
+ ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
1127
+ "info",
1128
+ `Optional events not subscribed: ${missingOptional.join(", ")}.`,
1129
+ { context: { missing: missingOptional.join(",") } }
1130
+ )
1131
+ );
1132
+ }
1133
+ return buildResult("webhook", mode, issues, match.attributes, {
1134
+ label: match.attributes.url,
1135
+ id: match.id,
1136
+ url: match.attributes.url
1126
1137
  });
1127
1138
  }
1128
1139
 
@@ -1273,10 +1284,7 @@ function renderCliError(message, hints = []) {
1273
1284
  function getDoctorHints(err) {
1274
1285
  if (!isFreshSqueezyError(err)) return [];
1275
1286
  if (err.code === "MISSING_API_KEY") {
1276
- return [
1277
- "fresh-squeezy init",
1278
- `export ${ENV_KEYS.apiKey}=ls_live_or_test_key`
1279
- ];
1287
+ return ["fresh-squeezy init", `export ${ENV_KEYS.apiKey}=ls_live_or_test_key`];
1280
1288
  }
1281
1289
  if (err.code === "INVALID_MODE") {
1282
1290
  return ["fresh-squeezy doctor --mode test", "fresh-squeezy doctor --mode live"];
@@ -1289,10 +1297,7 @@ function getDoctorHints(err) {
1289
1297
  function getValidateHints(err, target) {
1290
1298
  if (!isFreshSqueezyError(err)) return [];
1291
1299
  if (err.code === "MISSING_API_KEY") {
1292
- return [
1293
- "fresh-squeezy init",
1294
- `export ${ENV_KEYS.apiKey}=ls_live_or_test_key`
1295
- ];
1300
+ return ["fresh-squeezy init", `export ${ENV_KEYS.apiKey}=ls_live_or_test_key`];
1296
1301
  }
1297
1302
  if (err.code === "INVALID_MODE") {
1298
1303
  return [
@@ -1336,6 +1341,49 @@ function isFreshSqueezyError(err) {
1336
1341
  return err instanceof FreshSqueezyError;
1337
1342
  }
1338
1343
 
1344
+ // src/cli/render.ts
1345
+ import chalk from "chalk";
1346
+ function renderResult(result) {
1347
+ const lines = [];
1348
+ const badge = result.ok ? chalk.green("PASS") : chalk.red("FAIL");
1349
+ const mode = chalk.dim(`[${result.mode}]`);
1350
+ const target = renderTarget(result.target);
1351
+ const suffix = target ? ` ${chalk.dim(target)}` : "";
1352
+ lines.push(`${badge} ${mode} ${chalk.bold(result.name)}${suffix}`);
1353
+ for (const issue2 of result.issues) {
1354
+ lines.push(` ${renderIssueLine(issue2)}`);
1355
+ if (issue2.suggestedFix) {
1356
+ lines.push(` ${chalk.dim("fix:")} ${issue2.suggestedFix}`);
1357
+ }
1358
+ }
1359
+ return lines.join("\n");
1360
+ }
1361
+ function renderTarget(target) {
1362
+ if (!target) return "";
1363
+ const parts = [target.label];
1364
+ if (target.id) parts.push(`id ${target.id}`);
1365
+ if (target.url && target.url !== target.label) parts.push(target.url);
1366
+ return parts.join(" - ");
1367
+ }
1368
+ function renderReport(report) {
1369
+ const header = report.ok ? chalk.green.bold("fresh-squeezy doctor: OK") : chalk.red.bold("fresh-squeezy doctor: FAILED");
1370
+ const failed = report.results.filter((result) => !result.ok).length;
1371
+ const summary = failed === 0 ? chalk.dim(`${report.results.length} checks passed`) : chalk.dim(`${failed}/${report.results.length} checks failed`);
1372
+ const body = report.results.map(renderResult).join("\n\n");
1373
+ return `${header} ${chalk.dim(`(mode: ${report.mode})`)}
1374
+ ${summary}
1375
+
1376
+ ${body}`;
1377
+ }
1378
+ function renderIssueLine(issue2) {
1379
+ const label = issue2.severity === "error" ? chalk.red("\u2717 error") : issue2.severity === "warning" ? chalk.yellow("! warn ") : chalk.blue("i info ");
1380
+ return `${label} ${chalk.gray(`[${issue2.code}]`)} ${issue2.message}`;
1381
+ }
1382
+
1383
+ // src/cli/prompts.ts
1384
+ import path from "path";
1385
+ import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
1386
+
1339
1387
  // src/cli/resourceDiscovery.ts
1340
1388
  var EMPTY_INIT_RESOURCE_CHOICES = {
1341
1389
  products: { choices: [] },
@@ -1464,116 +1512,80 @@ function toSubscriptionPlanChoice(variant, productName) {
1464
1512
  };
1465
1513
  }
1466
1514
 
1467
- // src/cli/render.ts
1468
- import chalk from "chalk";
1469
- function renderResult(result) {
1470
- const lines = [];
1471
- const badge = result.ok ? chalk.green("PASS") : chalk.red("FAIL");
1472
- const mode = chalk.dim(`[${result.mode}]`);
1473
- const target = renderTarget(result.target);
1474
- const suffix = target ? ` ${chalk.dim(target)}` : "";
1475
- lines.push(`${badge} ${mode} ${chalk.bold(result.name)}${suffix}`);
1476
- for (const issue2 of result.issues) {
1477
- lines.push(` ${renderIssueLine(issue2)}`);
1478
- if (issue2.suggestedFix) {
1479
- lines.push(` ${chalk.dim("fix:")} ${issue2.suggestedFix}`);
1480
- }
1481
- }
1482
- return lines.join("\n");
1483
- }
1484
- function renderTarget(target) {
1485
- if (!target) return "";
1486
- const parts = [target.label];
1487
- if (target.id) parts.push(`id ${target.id}`);
1488
- if (target.url && target.url !== target.label) parts.push(target.url);
1489
- return parts.join(" - ");
1490
- }
1491
- function renderReport(report) {
1492
- const header = report.ok ? chalk.green.bold("fresh-squeezy doctor: OK") : chalk.red.bold("fresh-squeezy doctor: FAILED");
1493
- const failed = report.results.filter((result) => !result.ok).length;
1494
- const summary = failed === 0 ? chalk.dim(`${report.results.length} checks passed`) : chalk.dim(`${failed}/${report.results.length} checks failed`);
1495
- const body = report.results.map(renderResult).join("\n\n");
1496
- return `${header} ${chalk.dim(`(mode: ${report.mode})`)}
1497
- ${summary}
1498
-
1499
- ${body}`;
1500
- }
1501
- function renderIssueLine(issue2) {
1502
- const label = issue2.severity === "error" ? chalk.red("\u2717 error") : issue2.severity === "warning" ? chalk.yellow("! warn ") : chalk.blue("i info ");
1503
- return `${label} ${chalk.gray(`[${issue2.code}]`)} ${issue2.message}`;
1504
- }
1505
-
1506
1515
  // src/cli/prompts.ts
1507
- import path from "path";
1508
- import inquirer from "inquirer";
1509
1516
  var PROMPT_THEME = {
1510
1517
  prefix: {
1511
1518
  idle: "\u203A",
1512
1519
  done: "\u2713"
1513
1520
  }
1514
1521
  };
1522
+ async function pickLauncherAction() {
1523
+ return select({
1524
+ message: "What do you want to do?",
1525
+ theme: PROMPT_THEME,
1526
+ choices: [
1527
+ {
1528
+ name: "Start guided setup \u2014 key, store, checks, doctor",
1529
+ value: "init"
1530
+ },
1531
+ {
1532
+ name: "Run doctor now \u2014 pick stores interactively when needed",
1533
+ value: "doctor"
1534
+ },
1535
+ {
1536
+ name: "Show command examples \u2014 copy/paste friendly",
1537
+ value: "examples"
1538
+ },
1539
+ {
1540
+ name: "Exit",
1541
+ value: "exit"
1542
+ }
1543
+ ]
1544
+ });
1545
+ }
1515
1546
  async function askForApiKey() {
1516
- const answers = await inquirer.prompt([
1517
- {
1518
- type: "password",
1519
- name: "apiKey",
1520
- message: "Paste your Lemon Squeezy API key:",
1521
- mask: "",
1522
- theme: PROMPT_THEME,
1523
- validate: (value) => value.trim().length > 0 ? true : "API key is required."
1524
- }
1525
- ]);
1526
- return { apiKey: answers.apiKey.trim() };
1547
+ const apiKey = await password({
1548
+ message: "Paste your Lemon Squeezy API key:",
1549
+ mask: false,
1550
+ theme: PROMPT_THEME,
1551
+ validate: (value) => value.trim().length > 0 ? true : "API key is required."
1552
+ });
1553
+ return { apiKey: apiKey.trim() };
1527
1554
  }
1528
1555
  async function pickStore(choices) {
1529
- const { storeId } = await inquirer.prompt([
1530
- {
1531
- type: "list",
1532
- name: "storeId",
1533
- message: "Pick a store to validate against:",
1534
- theme: PROMPT_THEME,
1535
- choices: choices.map((entry) => ({
1536
- name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
1537
- value: entry.id
1538
- }))
1539
- }
1540
- ]);
1541
- return storeId;
1556
+ return select({
1557
+ message: "Pick a store to validate against:",
1558
+ theme: PROMPT_THEME,
1559
+ choices: choices.map((entry) => ({
1560
+ name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
1561
+ value: entry.id
1562
+ }))
1563
+ });
1542
1564
  }
1543
1565
  async function pickStores(choices) {
1544
- const { storeIds } = await inquirer.prompt([
1545
- {
1546
- type: "checkbox",
1547
- name: "storeIds",
1548
- message: "Pick one or more stores (space to toggle, enter to confirm):",
1549
- theme: PROMPT_THEME,
1550
- choices: choices.map((entry, index) => ({
1551
- name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
1552
- value: entry.id,
1553
- checked: index === 0
1554
- })),
1555
- validate: (values2) => Array.isArray(values2) && values2.length > 0 ? true : "Pick at least one store."
1556
- }
1557
- ]);
1558
- return storeIds;
1566
+ return checkbox({
1567
+ message: "Pick one or more stores (space to toggle, enter to confirm):",
1568
+ theme: PROMPT_THEME,
1569
+ choices: choices.map((entry, index) => ({
1570
+ name: `${entry.name} (${entry.slug}) \u2014 id ${entry.id}`,
1571
+ value: entry.id,
1572
+ checked: index === 0
1573
+ })),
1574
+ validate: (selected) => selected.length > 0 ? true : "Pick at least one store."
1575
+ });
1559
1576
  }
1560
1577
  async function selectDoctorTargets() {
1561
- const { targets } = await inquirer.prompt([
1562
- {
1563
- type: "checkbox",
1564
- name: "targets",
1565
- message: "Add resource checks to this doctor run?",
1566
- theme: PROMPT_THEME,
1567
- choices: [
1568
- { name: "Product checkout", value: "product" },
1569
- { name: "Webhook registration", value: "webhook" },
1570
- { name: "Discount code", value: "discount" },
1571
- { name: "License key", value: "license-key" },
1572
- { name: "Subscription plan", value: "subscription-plan" }
1573
- ]
1574
- }
1575
- ]);
1576
- return targets;
1578
+ return checkbox({
1579
+ message: "Add resource checks to this doctor run?",
1580
+ theme: PROMPT_THEME,
1581
+ choices: [
1582
+ { name: "Product checkout", value: "product" },
1583
+ { name: "Webhook registration", value: "webhook" },
1584
+ { name: "Discount code", value: "discount" },
1585
+ { name: "License key", value: "license-key" },
1586
+ { name: "Subscription plan", value: "subscription-plan" }
1587
+ ]
1588
+ });
1577
1589
  }
1578
1590
  async function askForDoctorTargetValues(targets, choices = EMPTY_INIT_RESOURCE_CHOICES) {
1579
1591
  if (targets.length === 0) return {};
@@ -1639,9 +1651,12 @@ async function askForDoctorTargetValues(targets, choices = EMPTY_INIT_RESOURCE_C
1639
1651
  validate: optional("Variant ID")
1640
1652
  });
1641
1653
  }
1642
- if (manualQuestions.length > 0) {
1643
- const manualAnswers = await inquirer.prompt(manualQuestions);
1644
- Object.assign(answers, manualAnswers);
1654
+ for (const question of manualQuestions) {
1655
+ answers[question.field] = await input({
1656
+ message: question.message,
1657
+ theme: PROMPT_THEME,
1658
+ validate: question.validate
1659
+ });
1645
1660
  }
1646
1661
  return {
1647
1662
  productIds: cleanList(answers.productIds),
@@ -1652,81 +1667,58 @@ async function askForDoctorTargetValues(targets, choices = EMPTY_INIT_RESOURCE_C
1652
1667
  };
1653
1668
  }
1654
1669
  async function confirmLiveModeRun() {
1655
- const { continueLive } = await inquirer.prompt([
1656
- {
1657
- type: "confirm",
1658
- name: "continueLive",
1659
- message: "This is a live-mode key. Continue with live checks?",
1660
- default: false,
1661
- theme: PROMPT_THEME
1662
- }
1663
- ]);
1664
- return continueLive;
1670
+ return confirm({
1671
+ message: "This is a live-mode key. Continue with live checks?",
1672
+ default: false,
1673
+ theme: PROMPT_THEME
1674
+ });
1665
1675
  }
1666
1676
  async function confirmWriteEnvFile(filePath) {
1667
- const { confirm } = await inquirer.prompt([
1668
- {
1669
- type: "confirm",
1670
- name: "confirm",
1671
- message: `Write these values to ${formatPromptPath(filePath)}?`,
1672
- default: true,
1673
- theme: PROMPT_THEME
1674
- }
1675
- ]);
1676
- return confirm;
1677
- }
1678
- async function resolveTargetValue(input) {
1679
- if (input.group.choices.length === 0) {
1680
- input.manualQuestions.push({
1681
- type: "input",
1682
- name: input.field,
1683
- message: input.manualMessage,
1684
- theme: PROMPT_THEME,
1685
- validate: input.validate
1677
+ return confirm({
1678
+ message: `Write these values to ${formatPromptPath(filePath)}?`,
1679
+ default: true,
1680
+ theme: PROMPT_THEME
1681
+ });
1682
+ }
1683
+ async function resolveTargetValue(input2) {
1684
+ if (input2.group.choices.length === 0) {
1685
+ input2.manualQuestions.push({
1686
+ field: input2.field,
1687
+ message: input2.manualMessage,
1688
+ validate: input2.validate
1686
1689
  });
1687
1690
  return;
1688
1691
  }
1689
- const { values: values2 } = await inquirer.prompt([
1690
- {
1691
- type: "checkbox",
1692
- name: "values",
1693
- message: input.pickMessage,
1694
- theme: PROMPT_THEME,
1695
- choices: input.group.choices.map((choice) => ({
1696
- name: choice.label,
1697
- value: choice.value
1698
- }))
1699
- }
1700
- ]);
1692
+ const values2 = await checkbox({
1693
+ message: input2.pickMessage,
1694
+ theme: PROMPT_THEME,
1695
+ choices: input2.group.choices.map((choice) => ({
1696
+ name: choice.label,
1697
+ value: choice.value
1698
+ }))
1699
+ });
1701
1700
  if (values2.length > 0) {
1702
- input.answers[input.field] = values2;
1701
+ input2.answers[input2.field] = values2;
1703
1702
  return;
1704
1703
  }
1705
- const action = await askEmptyTargetAction(input.emptyMessage);
1704
+ const action = await askEmptyTargetAction(input2.emptyMessage);
1706
1705
  if (action === "manual") {
1707
- input.manualQuestions.push({
1708
- type: "input",
1709
- name: input.field,
1710
- message: input.manualMessage,
1711
- theme: PROMPT_THEME,
1712
- validate: input.validate
1706
+ input2.manualQuestions.push({
1707
+ field: input2.field,
1708
+ message: input2.manualMessage,
1709
+ validate: input2.validate
1713
1710
  });
1714
1711
  }
1715
1712
  }
1716
1713
  async function askEmptyTargetAction(message) {
1717
- const { action } = await inquirer.prompt([
1718
- {
1719
- type: "list",
1720
- name: "action",
1721
- message,
1722
- theme: PROMPT_THEME,
1723
- choices: [
1724
- { name: "Enter manually", value: "manual" },
1725
- { name: "Skip this check", value: "skip" }
1726
- ]
1727
- }
1728
- ]);
1729
- return action;
1714
+ return select({
1715
+ message,
1716
+ theme: PROMPT_THEME,
1717
+ choices: [
1718
+ { name: "Enter manually", value: "manual" },
1719
+ { name: "Skip this check", value: "skip" }
1720
+ ]
1721
+ });
1730
1722
  }
1731
1723
  function required(label) {
1732
1724
  return (value) => value.trim().length > 0 ? true : `${label} is required.`;
@@ -1772,11 +1764,11 @@ function isPromptCancel(error) {
1772
1764
  }
1773
1765
 
1774
1766
  // src/cli/resolveStores.ts
1775
- async function resolveStores(client, input) {
1776
- if (input.storeIds && input.storeIds.length > 0) {
1777
- return { storeIds: dedupe(input.storeIds), skipped: false };
1767
+ async function resolveStores(client, input2) {
1768
+ if (input2.storeIds && input2.storeIds.length > 0) {
1769
+ return { storeIds: dedupe(input2.storeIds), skipped: false };
1778
1770
  }
1779
- if (input.allStores) {
1771
+ if (input2.allStores) {
1780
1772
  const ids2 = await fetchReachableStoreIds(client);
1781
1773
  if (ids2.length === 0) {
1782
1774
  throw new FreshSqueezyError({
@@ -1786,7 +1778,7 @@ async function resolveStores(client, input) {
1786
1778
  }
1787
1779
  return { storeIds: ids2, skipped: false };
1788
1780
  }
1789
- if (!input.isInteractive) {
1781
+ if (!input2.isInteractive) {
1790
1782
  return { storeIds: [], skipped: true };
1791
1783
  }
1792
1784
  const ids = await fetchReachableStoreIds(client);
@@ -1970,7 +1962,10 @@ async function runConnectionOnly(client, options) {
1970
1962
  }
1971
1963
  function writeFatal(err, asJson) {
1972
1964
  if (asJson) {
1973
- 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) } };
1965
+ const payload = err instanceof FreshSqueezyError ? { ok: false, error: { code: err.code, message: err.message, status: err.status ?? null } } : {
1966
+ ok: false,
1967
+ error: { code: "UNKNOWN", message: err instanceof Error ? err.message : String(err) }
1968
+ };
1974
1969
  process.stderr.write(`${JSON.stringify(payload)}
1975
1970
  `);
1976
1971
  return;
@@ -1979,8 +1974,10 @@ function writeFatal(err, asJson) {
1979
1974
  process.stderr.write(renderCliError(message, getDoctorHints(err)));
1980
1975
  }
1981
1976
 
1982
- // src/cli/commands/launcher.ts
1983
- import chalk4 from "chalk";
1977
+ // src/cli/commands/init.ts
1978
+ import fs from "fs/promises";
1979
+ import path2 from "path";
1980
+ import chalk3 from "chalk";
1984
1981
 
1985
1982
  // src/cli/brand.ts
1986
1983
  import chalk2 from "chalk";
@@ -2027,100 +2024,7 @@ function renderCancelMessage() {
2027
2024
  `;
2028
2025
  }
2029
2026
 
2030
- // src/cli/projectInstall.ts
2031
- import { spawn } from "child_process";
2032
- import fs from "fs/promises";
2033
- import path2 from "path";
2034
- var PACKAGE_NAME = "fresh-squeezy";
2035
- async function ensureFreshSqueezyDevDependency(options = {}) {
2036
- if (options.skipInstall) {
2037
- return { status: "skipped" };
2038
- }
2039
- const cwd = options.cwd ?? process.cwd();
2040
- const packageJsonPath = await findPackageJson(cwd);
2041
- if (!packageJsonPath) {
2042
- return { status: "no-package-json" };
2043
- }
2044
- const projectDir = path2.dirname(packageJsonPath);
2045
- const manifest = await readPackageJson(packageJsonPath);
2046
- if (manifest.name === PACKAGE_NAME) {
2047
- return { status: "self-package", projectDir, packageJsonPath };
2048
- }
2049
- const packageManager = await detectPackageManager(projectDir);
2050
- if (hasFreshSqueezy(manifest)) {
2051
- return { status: "already-present", projectDir, packageJsonPath, packageManager };
2052
- }
2053
- const packageSpec = options.packageVersion ? `${PACKAGE_NAME}@${options.packageVersion}` : PACKAGE_NAME;
2054
- const command = buildInstallCommand(packageManager, packageSpec);
2055
- const executable = command[0];
2056
- if (!executable) throw new Error("No package manager command resolved.");
2057
- const runCommand = options.runCommand ?? runPackageCommand;
2058
- const code = await runCommand(executable, command.slice(1), { cwd: projectDir });
2059
- if (code !== 0) {
2060
- throw new Error(`${command.join(" ")} failed with exit code ${code}`);
2061
- }
2062
- return { status: "installed", projectDir, packageJsonPath, packageManager, command };
2063
- }
2064
- async function findPackageJson(cwd) {
2065
- let current = path2.resolve(cwd);
2066
- while (true) {
2067
- const candidate = path2.join(current, "package.json");
2068
- if (await pathExists(candidate)) return candidate;
2069
- const parent = path2.dirname(current);
2070
- if (parent === current) return void 0;
2071
- current = parent;
2072
- }
2073
- }
2074
- async function readPackageJson(packageJsonPath) {
2075
- const raw = await fs.readFile(packageJsonPath, "utf8");
2076
- return JSON.parse(raw);
2077
- }
2078
- async function detectPackageManager(projectDir) {
2079
- if (await pathExists(path2.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
2080
- if (await pathExists(path2.join(projectDir, "yarn.lock"))) return "yarn";
2081
- if (await pathExists(path2.join(projectDir, "bun.lock")) || await pathExists(path2.join(projectDir, "bun.lockb"))) {
2082
- return "bun";
2083
- }
2084
- return "npm";
2085
- }
2086
- function hasFreshSqueezy(manifest) {
2087
- return [
2088
- manifest.dependencies,
2089
- manifest.devDependencies,
2090
- manifest.optionalDependencies,
2091
- manifest.peerDependencies
2092
- ].some((section) => section?.[PACKAGE_NAME] !== void 0);
2093
- }
2094
- function buildInstallCommand(packageManager, packageSpec) {
2095
- if (packageManager === "pnpm") return ["pnpm", "add", "-D", packageSpec];
2096
- if (packageManager === "yarn") return ["yarn", "add", "-D", packageSpec];
2097
- if (packageManager === "bun") return ["bun", "add", "-d", packageSpec];
2098
- return ["npm", "install", "--save-dev", packageSpec];
2099
- }
2100
- function runPackageCommand(command, args, options) {
2101
- return new Promise((resolve2, reject) => {
2102
- const child = spawn(command, args, {
2103
- cwd: options.cwd,
2104
- stdio: "inherit",
2105
- shell: false
2106
- });
2107
- child.on("error", reject);
2108
- child.on("close", (code) => resolve2(code ?? 1));
2109
- });
2110
- }
2111
- async function pathExists(filePath) {
2112
- try {
2113
- await fs.access(filePath);
2114
- return true;
2115
- } catch {
2116
- return false;
2117
- }
2118
- }
2119
-
2120
2027
  // src/cli/commands/init.ts
2121
- import fs2 from "fs/promises";
2122
- import path3 from "path";
2123
- import chalk3 from "chalk";
2124
2028
  async function runInitCommand(options = {}) {
2125
2029
  try {
2126
2030
  return await runInitFlow(options);
@@ -2164,7 +2068,9 @@ async function runInitFlow(options) {
2164
2068
  const storeIds = connection.resource?.storeIds ?? [];
2165
2069
  if (storeIds.length === 0) {
2166
2070
  process.stdout.write(
2167
- chalk3.yellow("No stores reachable with this key. Create a store in Lemon Squeezy and retry.\n")
2071
+ chalk3.yellow(
2072
+ "No stores reachable with this key. Create a store in Lemon Squeezy and retry.\n"
2073
+ )
2168
2074
  );
2169
2075
  return 1;
2170
2076
  }
@@ -2177,7 +2083,9 @@ async function runInitFlow(options) {
2177
2083
  })).filter((entry) => entry.id !== "");
2178
2084
  if (pickable.length === 0) {
2179
2085
  process.stdout.write(
2180
- chalk3.yellow("Stores were discovered, but none could be validated. Check account access and retry.\n")
2086
+ chalk3.yellow(
2087
+ "Stores were discovered, but none could be validated. Check account access and retry.\n"
2088
+ )
2181
2089
  );
2182
2090
  return 1;
2183
2091
  }
@@ -2188,7 +2096,7 @@ async function runInitFlow(options) {
2188
2096
  const resourceChoices = await discoverChoices(client, storeId, selectedTargets);
2189
2097
  process.stdout.write(renderDiscoverySummary(resourceChoices, selectedTargets));
2190
2098
  const doctorTargets = await askForDoctorTargetValues(selectedTargets, resourceChoices);
2191
- const envPath = path3.resolve(process.cwd(), options.envFile ?? ".env.local");
2099
+ const envPath = path2.resolve(process.cwd(), options.envFile ?? ".env");
2192
2100
  const checkNames = getDoctorCheckNames(doctorTargets);
2193
2101
  process.stdout.write(renderSetupSummary({ envPath, mode, storeId, checkNames }));
2194
2102
  const shouldWrite = await confirmWriteEnvFile(envPath);
@@ -2210,19 +2118,22 @@ async function resolveStoreSelection(pickable) {
2210
2118
  if (pickable.length === 1) {
2211
2119
  const store = pickable[0];
2212
2120
  if (!store) throw new Error("Expected one reachable store.");
2213
- process.stdout.write(renderDetected("Store", `${store.name} (${store.slug})`, `id ${store.id}`));
2121
+ process.stdout.write(
2122
+ renderDetected("Store", `${store.name} (${store.slug})`, `id ${store.id}`)
2123
+ );
2214
2124
  return store.id;
2215
2125
  }
2216
2126
  return pickStore(pickable);
2217
2127
  }
2218
2128
  async function discoverChoices(client, storeId, selectedTargets) {
2219
- if (selectedTargets.length === 0) return {
2220
- products: { choices: [] },
2221
- webhooks: { choices: [] },
2222
- discounts: { choices: [] },
2223
- licenseKeys: { choices: [] },
2224
- subscriptionPlans: { choices: [] }
2225
- };
2129
+ if (selectedTargets.length === 0)
2130
+ return {
2131
+ products: { choices: [] },
2132
+ webhooks: { choices: [] },
2133
+ discounts: { choices: [] },
2134
+ licenseKeys: { choices: [] },
2135
+ subscriptionPlans: { choices: [] }
2136
+ };
2226
2137
  return discoverInitResourceChoices(client, storeId, selectedTargets);
2227
2138
  }
2228
2139
  async function resolveApiKey() {
@@ -2255,8 +2166,10 @@ async function createDetectedClient(apiKey) {
2255
2166
  return { client, mode: actualMode, connection };
2256
2167
  }
2257
2168
  if (envMode) {
2258
- process.stdout.write(chalk3.dim(`Using ${ENV_KEYS.mode}=${envMode}; API mode was not exposed.
2259
- `));
2169
+ process.stdout.write(
2170
+ chalk3.dim(`Using ${ENV_KEYS.mode}=${envMode}; API mode was not exposed.
2171
+ `)
2172
+ );
2260
2173
  return { client, mode: envMode, connection };
2261
2174
  }
2262
2175
  process.stdout.write(chalk3.dim("Could not auto-detect key mode; using test mode.\n"));
@@ -2266,8 +2179,10 @@ function parseEnvMode() {
2266
2179
  const value = process.env[ENV_KEYS.mode]?.trim();
2267
2180
  if (value === "test" || value === "live") return value;
2268
2181
  if (value) {
2269
- process.stdout.write(chalk3.yellow(`Ignoring invalid ${ENV_KEYS.mode}=${value}; expected test or live.
2270
- `));
2182
+ process.stdout.write(
2183
+ chalk3.yellow(`Ignoring invalid ${ENV_KEYS.mode}=${value}; expected test or live.
2184
+ `)
2185
+ );
2271
2186
  }
2272
2187
  return void 0;
2273
2188
  }
@@ -2284,13 +2199,13 @@ function pushCheckName(names, name, values2) {
2284
2199
  if (!values2 || values2.length === 0) return;
2285
2200
  names.push(values2.length === 1 ? name : `${name} x${values2.length}`);
2286
2201
  }
2287
- function renderSetupSummary(input) {
2202
+ function renderSetupSummary(input2) {
2288
2203
  return [
2289
2204
  renderStep(5, 5, "Ready to verify", "review before writing env"),
2290
- ` ${chalk3.dim("mode")} ${input.mode}`,
2291
- ` ${chalk3.dim("store")} ${input.storeId}`,
2292
- ` ${chalk3.dim("checks")} ${input.checkNames.join(", ")}`,
2293
- ` ${chalk3.dim("env")} ${input.envPath}`,
2205
+ ` ${chalk3.dim("mode")} ${input2.mode}`,
2206
+ ` ${chalk3.dim("store")} ${input2.storeId}`,
2207
+ ` ${chalk3.dim("checks")} ${input2.checkNames.join(", ")}`,
2208
+ ` ${chalk3.dim("env")} ${input2.envPath}`,
2294
2209
  ""
2295
2210
  ].join("\n");
2296
2211
  }
@@ -2318,13 +2233,13 @@ async function writeEnvFile(envPath, values2) {
2318
2233
  next.push(`${key}=${value}`);
2319
2234
  }
2320
2235
  }
2321
- await fs2.mkdir(path3.dirname(envPath), { recursive: true });
2322
- await fs2.writeFile(envPath, `${next.join("\n").replace(/\n*$/, "")}
2236
+ await fs.mkdir(path2.dirname(envPath), { recursive: true });
2237
+ await fs.writeFile(envPath, `${next.join("\n").replace(/\n*$/, "")}
2323
2238
  `, { encoding: "utf8" });
2324
2239
  }
2325
2240
  async function readEnvFile(envPath) {
2326
2241
  try {
2327
- return await fs2.readFile(envPath, "utf8");
2242
+ return await fs.readFile(envPath, "utf8");
2328
2243
  } catch (err) {
2329
2244
  if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
2330
2245
  return "";
@@ -2347,13 +2262,108 @@ function renderDiscoverySummary(choices, selectedTargets) {
2347
2262
  if (group.error) {
2348
2263
  lines.push(chalk3.yellow(` ! ${label} discovery failed; manual entry is available.`));
2349
2264
  } else {
2350
- lines.push(renderDetected(label, String(group.choices.length), "Lemon Squeezy API").trimEnd());
2265
+ lines.push(
2266
+ renderDetected(label, String(group.choices.length), "Lemon Squeezy API").trimEnd()
2267
+ );
2351
2268
  }
2352
2269
  }
2353
2270
  return `${lines.join("\n")}
2354
2271
  `;
2355
2272
  }
2356
2273
 
2274
+ // src/cli/commands/launcher.ts
2275
+ import chalk4 from "chalk";
2276
+
2277
+ // src/cli/projectInstall.ts
2278
+ import { spawn } from "child_process";
2279
+ import fs2 from "fs/promises";
2280
+ import path3 from "path";
2281
+ var PACKAGE_NAME = "fresh-squeezy";
2282
+ async function ensureFreshSqueezyDevDependency(options = {}) {
2283
+ if (options.skipInstall) {
2284
+ return { status: "skipped" };
2285
+ }
2286
+ const cwd = options.cwd ?? process.cwd();
2287
+ const packageJsonPath = await findPackageJson(cwd);
2288
+ if (!packageJsonPath) {
2289
+ return { status: "no-package-json" };
2290
+ }
2291
+ const projectDir = path3.dirname(packageJsonPath);
2292
+ const manifest = await readPackageJson(packageJsonPath);
2293
+ if (manifest.name === PACKAGE_NAME) {
2294
+ return { status: "self-package", projectDir, packageJsonPath };
2295
+ }
2296
+ const packageManager = await detectPackageManager(projectDir);
2297
+ if (hasFreshSqueezy(manifest)) {
2298
+ return { status: "already-present", projectDir, packageJsonPath, packageManager };
2299
+ }
2300
+ const packageSpec = options.packageVersion ? `${PACKAGE_NAME}@${options.packageVersion}` : PACKAGE_NAME;
2301
+ const command = buildInstallCommand(packageManager, packageSpec);
2302
+ const executable = command[0];
2303
+ if (!executable) throw new Error("No package manager command resolved.");
2304
+ const runCommand = options.runCommand ?? runPackageCommand;
2305
+ const code = await runCommand(executable, command.slice(1), { cwd: projectDir });
2306
+ if (code !== 0) {
2307
+ throw new Error(`${command.join(" ")} failed with exit code ${code}`);
2308
+ }
2309
+ return { status: "installed", projectDir, packageJsonPath, packageManager, command };
2310
+ }
2311
+ async function findPackageJson(cwd) {
2312
+ let current = path3.resolve(cwd);
2313
+ while (true) {
2314
+ const candidate = path3.join(current, "package.json");
2315
+ if (await pathExists(candidate)) return candidate;
2316
+ const parent = path3.dirname(current);
2317
+ if (parent === current) return void 0;
2318
+ current = parent;
2319
+ }
2320
+ }
2321
+ async function readPackageJson(packageJsonPath) {
2322
+ const raw = await fs2.readFile(packageJsonPath, "utf8");
2323
+ return JSON.parse(raw);
2324
+ }
2325
+ async function detectPackageManager(projectDir) {
2326
+ if (await pathExists(path3.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
2327
+ if (await pathExists(path3.join(projectDir, "yarn.lock"))) return "yarn";
2328
+ if (await pathExists(path3.join(projectDir, "bun.lock")) || await pathExists(path3.join(projectDir, "bun.lockb"))) {
2329
+ return "bun";
2330
+ }
2331
+ return "npm";
2332
+ }
2333
+ function hasFreshSqueezy(manifest) {
2334
+ return [
2335
+ manifest.dependencies,
2336
+ manifest.devDependencies,
2337
+ manifest.optionalDependencies,
2338
+ manifest.peerDependencies
2339
+ ].some((section) => section?.[PACKAGE_NAME] !== void 0);
2340
+ }
2341
+ function buildInstallCommand(packageManager, packageSpec) {
2342
+ if (packageManager === "pnpm") return ["pnpm", "add", "-D", packageSpec];
2343
+ if (packageManager === "yarn") return ["yarn", "add", "-D", packageSpec];
2344
+ if (packageManager === "bun") return ["bun", "add", "-d", packageSpec];
2345
+ return ["npm", "install", "--save-dev", packageSpec];
2346
+ }
2347
+ function runPackageCommand(command, args, options) {
2348
+ return new Promise((resolve2, reject) => {
2349
+ const child = spawn(command, args, {
2350
+ cwd: options.cwd,
2351
+ stdio: "inherit",
2352
+ shell: false
2353
+ });
2354
+ child.on("error", reject);
2355
+ child.on("close", (code) => resolve2(code ?? 1));
2356
+ });
2357
+ }
2358
+ async function pathExists(filePath) {
2359
+ try {
2360
+ await fs2.access(filePath);
2361
+ return true;
2362
+ } catch {
2363
+ return false;
2364
+ }
2365
+ }
2366
+
2357
2367
  // src/cli/commands/launcher.ts
2358
2368
  async function runLauncherCommand(options) {
2359
2369
  if (!options.isInteractive) return 2;
@@ -2370,8 +2380,10 @@ async function runLauncherCommand(options) {
2370
2380
  packageVersion: options.packageVersion
2371
2381
  });
2372
2382
  process.stdout.write(renderProjectInstallResult(installResult));
2373
- process.stdout.write(renderStep(2, 2, "Guided setup", "detect credentials, stores, and resources"));
2374
- return runInitCommand({ isInteractive: true });
2383
+ if (!process.env[ENV_KEYS.apiKey]?.trim()) {
2384
+ return startGuidedSetup();
2385
+ }
2386
+ return routeLauncherAction(await pickLauncherAction());
2375
2387
  } catch (err) {
2376
2388
  if (isPromptCancel(err)) {
2377
2389
  process.stderr.write(renderCancelMessage());
@@ -2385,6 +2397,25 @@ async function runLauncherCommand(options) {
2385
2397
  return 1;
2386
2398
  }
2387
2399
  }
2400
+ async function routeLauncherAction(action) {
2401
+ switch (action) {
2402
+ case "init":
2403
+ return startGuidedSetup();
2404
+ case "doctor":
2405
+ return runDoctorCommand({ isInteractive: true });
2406
+ case "examples":
2407
+ process.stdout.write(renderCommandExamples());
2408
+ return 0;
2409
+ case "exit":
2410
+ return 0;
2411
+ }
2412
+ }
2413
+ function startGuidedSetup() {
2414
+ process.stdout.write(
2415
+ renderStep(2, 2, "Guided setup", "detect credentials, stores, and resources")
2416
+ );
2417
+ return runInitCommand({ isInteractive: true });
2418
+ }
2388
2419
  function renderProjectInstallResult(result) {
2389
2420
  if (result.status === "installed") {
2390
2421
  return renderDetected(
@@ -2463,9 +2494,7 @@ async function runPerStore(client, target, storeIds, options) {
2463
2494
  }
2464
2495
  if (target === "webhook") {
2465
2496
  const url = required2(options.webhookUrl, "--webhook-url is required for `validate webhook`.");
2466
- return Promise.all(
2467
- storeIds.map((storeId) => client.validateWebhook({ storeId, url }))
2468
- );
2497
+ return Promise.all(storeIds.map((storeId) => client.validateWebhook({ storeId, url })));
2469
2498
  }
2470
2499
  return [];
2471
2500
  }
@@ -2478,18 +2507,36 @@ async function runProduct(client, options) {
2478
2507
  });
2479
2508
  }
2480
2509
  async function runDiscount(client, options) {
2481
- const discountId = required2(options.discountId, "--discount-id is required for `validate discount`.");
2482
- const storeId = required2(options.storeIds?.[0], "--store-ids is required for `validate discount`.");
2510
+ const discountId = required2(
2511
+ options.discountId,
2512
+ "--discount-id is required for `validate discount`."
2513
+ );
2514
+ const storeId = required2(
2515
+ options.storeIds?.[0],
2516
+ "--store-ids is required for `validate discount`."
2517
+ );
2483
2518
  return client.validateDiscount({ storeId, discountId });
2484
2519
  }
2485
2520
  async function runLicenseKey(client, options) {
2486
- const licenseKeyId = required2(options.licenseKeyId, "--license-key-id is required for `validate license-key`.");
2487
- const storeId = required2(options.storeIds?.[0], "--store-ids is required for `validate license-key`.");
2521
+ const licenseKeyId = required2(
2522
+ options.licenseKeyId,
2523
+ "--license-key-id is required for `validate license-key`."
2524
+ );
2525
+ const storeId = required2(
2526
+ options.storeIds?.[0],
2527
+ "--store-ids is required for `validate license-key`."
2528
+ );
2488
2529
  return client.validateLicenseKey({ storeId, licenseKeyId });
2489
2530
  }
2490
2531
  async function runSubscriptionPlan(client, options) {
2491
- const variantId = required2(options.variantId, "--variant-id is required for `validate subscription-plan`.");
2492
- const storeId = required2(options.storeIds?.[0], "--store-ids is required for `validate subscription-plan`.");
2532
+ const variantId = required2(
2533
+ options.variantId,
2534
+ "--variant-id is required for `validate subscription-plan`."
2535
+ );
2536
+ const storeId = required2(
2537
+ options.storeIds?.[0],
2538
+ "--store-ids is required for `validate subscription-plan`."
2539
+ );
2493
2540
  return client.validateSubscriptionPlan({ storeId, variantId });
2494
2541
  }
2495
2542
  function emit(result, options) {
@@ -2510,7 +2557,6 @@ function required2(value, message) {
2510
2557
  }
2511
2558
 
2512
2559
  // src/cli/main.ts
2513
- dotenv.config({ path: ".env.local" });
2514
2560
  dotenv.config();
2515
2561
  var isInteractive = Boolean(process.stdin.isTTY);
2516
2562
  var packageVersion = readPackageVersion();
@@ -2546,7 +2592,10 @@ program.action(async (opts) => {
2546
2592
  });
2547
2593
  process.exit(code);
2548
2594
  });
2549
- var doctor2 = program.command("doctor").description("Run the full integration doctor").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("--all-resources", "Discover and validate every supported resource in each selected store").option("--product-id <id>", "Product to validate").option("--webhook-url <url>", "Webhook URL to validate").option("--discount-id <id>", "Discount to validate").option("--license-key-id <id>", "License key to validate").option("--variant-id <id>", "Subscription plan variant to validate").option("--json", "Emit machine-readable JSON").action(async (opts) => {
2595
+ var doctor2 = program.command("doctor").description("Run the full integration doctor").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(
2596
+ "--all-resources",
2597
+ "Discover and validate every supported resource in each selected store"
2598
+ ).option("--product-id <id>", "Product to validate").option("--webhook-url <url>", "Webhook URL to validate").option("--discount-id <id>", "Discount to validate").option("--license-key-id <id>", "License key to validate").option("--variant-id <id>", "Subscription plan variant to validate").option("--json", "Emit machine-readable JSON").action(async (opts) => {
2550
2599
  const code = await runDoctorCommand({
2551
2600
  mode: opts.mode,
2552
2601
  storeIds: opts.storeIds,
@@ -2588,16 +2637,22 @@ Targets:
2588
2637
  );
2589
2638
  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));
2590
2639
  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));
2591
- 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));
2640
+ validate.command("product").description("Check a product is published with at least one variant").requiredOption("--product-id <id>", "Product ID to validate").option(
2641
+ "--store-ids <ids>",
2642
+ "Expected owning store IDs (first is used for cross-check)",
2643
+ parseCsv
2644
+ ).option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("product", opts));
2592
2645
  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));
2593
2646
  validate.command("discount").description("Check a discount code is valid and redeemable").requiredOption("--discount-id <id>", "Discount ID to validate").option("--store-ids <ids>", "Store ID for ownership check (first ID used)", parseCsv).option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("discount", opts));
2594
2647
  validate.command("license-key").description("Check a license key is active and not at its activation limit").requiredOption("--license-key-id <id>", "License key ID to validate").option("--store-ids <ids>", "Store ID for ownership check (first ID used)", parseCsv).option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("license-key", opts));
2595
2648
  validate.command("subscription-plan").description("Check a subscription plan variant has valid billing interval and trial config").requiredOption("--variant-id <id>", "Variant ID of the subscription plan").option("--store-ids <ids>", "Store ID for ownership check (first ID used)", parseCsv).option("-m, --mode <mode>", "test or live", parseMode).option("--json", "Emit machine-readable JSON").action(async (opts) => runValidate("subscription-plan", opts));
2596
- 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) => {
2649
+ program.command("init").description("Interactive setup: ask for credentials, pick a store, run doctor").option("--env-file <path>", "Where to write credentials (default: .env)").action(async (opts) => {
2597
2650
  const code = await runInitCommand({ envFile: opts.envFile, isInteractive });
2598
2651
  process.exit(code);
2599
2652
  });
2600
- var types = program.command("types").description("Type-augmentation utilities for the official Lemon Squeezy SDK and hand-rolled types");
2653
+ var types = program.command("types").description(
2654
+ "Type-augmentation utilities for the official Lemon Squeezy SDK and hand-rolled types"
2655
+ );
2601
2656
  types.command("augment").description(
2602
2657
  "Generate a .d.ts that intersects your Lemon Squeezy resource types with changelog fields fresh-squeezy tracks."
2603
2658
  ).option("--out <path>", "Output path (default: lemonsqueezy.augment.d.ts in CWD)").option("--force", "Emit the generic file even when @lemonsqueezy/lemonsqueezy.js is detected").action(async (opts) => {