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/README.es.md +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +677 -622
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +605 -578
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -185
- package/dist/index.d.ts +264 -185
- package/dist/index.js +597 -578
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/assets/fresh-squeezy-hero.png +0 -0
- package/bin/fresh-squeezy.js +0 -2
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
|
|
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
|
|
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(
|
|
243
|
-
const 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(
|
|
251
|
-
const storeIdRaw =
|
|
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:
|
|
257
|
-
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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.
|
|
559
|
+
ISSUE_CODES.MODE_MISMATCH,
|
|
646
560
|
"error",
|
|
647
|
-
`
|
|
561
|
+
`API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
|
|
648
562
|
{
|
|
649
|
-
suggestedFix:
|
|
650
|
-
context: {
|
|
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 (
|
|
569
|
+
if (stores.length === 0) {
|
|
656
570
|
issues.push(
|
|
657
571
|
issue(
|
|
658
|
-
ISSUE_CODES.
|
|
572
|
+
ISSUE_CODES.STORE_NOT_FOUND,
|
|
659
573
|
"warning",
|
|
660
|
-
"
|
|
661
|
-
{ suggestedFix: "
|
|
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
|
-
|
|
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
|
|
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
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
-
|
|
1668
|
-
{
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1701
|
+
input2.answers[input2.field] = values2;
|
|
1703
1702
|
return;
|
|
1704
1703
|
}
|
|
1705
|
-
const action = await askEmptyTargetAction(
|
|
1704
|
+
const action = await askEmptyTargetAction(input2.emptyMessage);
|
|
1706
1705
|
if (action === "manual") {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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,
|
|
1776
|
-
if (
|
|
1777
|
-
return { storeIds: dedupe(
|
|
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 (
|
|
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 (!
|
|
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 } } : {
|
|
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/
|
|
1983
|
-
import
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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)
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
2202
|
+
function renderSetupSummary(input2) {
|
|
2288
2203
|
return [
|
|
2289
2204
|
renderStep(5, 5, "Ready to verify", "review before writing env"),
|
|
2290
|
-
` ${chalk3.dim("mode")} ${
|
|
2291
|
-
` ${chalk3.dim("store")} ${
|
|
2292
|
-
` ${chalk3.dim("checks")} ${
|
|
2293
|
-
` ${chalk3.dim("env")} ${
|
|
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
|
|
2322
|
-
await
|
|
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
|
|
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(
|
|
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.
|
|
2374
|
-
|
|
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(
|
|
2482
|
-
|
|
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(
|
|
2487
|
-
|
|
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(
|
|
2492
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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) => {
|