@creem_io/cli 0.2.1

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.
Files changed (98) hide show
  1. package/README.md +155 -0
  2. package/dist/commands/checkouts.d.ts +3 -0
  3. package/dist/commands/checkouts.d.ts.map +1 -0
  4. package/dist/commands/checkouts.js +175 -0
  5. package/dist/commands/checkouts.js.map +1 -0
  6. package/dist/commands/config.d.ts +3 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +184 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/customers.d.ts +3 -0
  11. package/dist/commands/customers.d.ts.map +1 -0
  12. package/dist/commands/customers.js +246 -0
  13. package/dist/commands/customers.js.map +1 -0
  14. package/dist/commands/discounts.d.ts +3 -0
  15. package/dist/commands/discounts.d.ts.map +1 -0
  16. package/dist/commands/discounts.js +419 -0
  17. package/dist/commands/discounts.js.map +1 -0
  18. package/dist/commands/index.d.ts +12 -0
  19. package/dist/commands/index.d.ts.map +1 -0
  20. package/dist/commands/index.js +26 -0
  21. package/dist/commands/index.js.map +1 -0
  22. package/dist/commands/login.d.ts +3 -0
  23. package/dist/commands/login.d.ts.map +1 -0
  24. package/dist/commands/login.js +115 -0
  25. package/dist/commands/login.js.map +1 -0
  26. package/dist/commands/logout.d.ts +3 -0
  27. package/dist/commands/logout.d.ts.map +1 -0
  28. package/dist/commands/logout.js +61 -0
  29. package/dist/commands/logout.js.map +1 -0
  30. package/dist/commands/migrate.d.ts +3 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +1073 -0
  33. package/dist/commands/migrate.js.map +1 -0
  34. package/dist/commands/products.d.ts +3 -0
  35. package/dist/commands/products.d.ts.map +1 -0
  36. package/dist/commands/products.js +400 -0
  37. package/dist/commands/products.js.map +1 -0
  38. package/dist/commands/subscriptions.d.ts +3 -0
  39. package/dist/commands/subscriptions.d.ts.map +1 -0
  40. package/dist/commands/subscriptions.js +476 -0
  41. package/dist/commands/subscriptions.js.map +1 -0
  42. package/dist/commands/transactions.d.ts +3 -0
  43. package/dist/commands/transactions.d.ts.map +1 -0
  44. package/dist/commands/transactions.js +326 -0
  45. package/dist/commands/transactions.js.map +1 -0
  46. package/dist/commands/whoami.d.ts +3 -0
  47. package/dist/commands/whoami.d.ts.map +1 -0
  48. package/dist/commands/whoami.js +76 -0
  49. package/dist/commands/whoami.js.map +1 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +109 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/lib/api.d.ts +25 -0
  55. package/dist/lib/api.d.ts.map +1 -0
  56. package/dist/lib/api.js +77 -0
  57. package/dist/lib/api.js.map +1 -0
  58. package/dist/lib/auth.d.ts +28 -0
  59. package/dist/lib/auth.d.ts.map +1 -0
  60. package/dist/lib/auth.js +113 -0
  61. package/dist/lib/auth.js.map +1 -0
  62. package/dist/lib/config.d.ts +34 -0
  63. package/dist/lib/config.d.ts.map +1 -0
  64. package/dist/lib/config.js +144 -0
  65. package/dist/lib/config.js.map +1 -0
  66. package/dist/lib/env-cache.d.ts +10 -0
  67. package/dist/lib/env-cache.d.ts.map +1 -0
  68. package/dist/lib/env-cache.js +27 -0
  69. package/dist/lib/env-cache.js.map +1 -0
  70. package/dist/tui/command-bar.d.ts +10 -0
  71. package/dist/tui/command-bar.d.ts.map +1 -0
  72. package/dist/tui/command-bar.js +35 -0
  73. package/dist/tui/command-bar.js.map +1 -0
  74. package/dist/tui/engine.d.ts +3 -0
  75. package/dist/tui/engine.d.ts.map +1 -0
  76. package/dist/tui/engine.js +454 -0
  77. package/dist/tui/engine.js.map +1 -0
  78. package/dist/tui/index.d.ts +3 -0
  79. package/dist/tui/index.d.ts.map +1 -0
  80. package/dist/tui/index.js +6 -0
  81. package/dist/tui/index.js.map +1 -0
  82. package/dist/tui/keymap.d.ts +29 -0
  83. package/dist/tui/keymap.d.ts.map +1 -0
  84. package/dist/tui/keymap.js +143 -0
  85. package/dist/tui/keymap.js.map +1 -0
  86. package/dist/tui/renderer.d.ts +12 -0
  87. package/dist/tui/renderer.d.ts.map +1 -0
  88. package/dist/tui/renderer.js +379 -0
  89. package/dist/tui/renderer.js.map +1 -0
  90. package/dist/tui/types.d.ts +53 -0
  91. package/dist/tui/types.d.ts.map +1 -0
  92. package/dist/tui/types.js +24 -0
  93. package/dist/tui/types.js.map +1 -0
  94. package/dist/utils/output.d.ts +67 -0
  95. package/dist/utils/output.d.ts.map +1 -0
  96. package/dist/utils/output.js +245 -0
  97. package/dist/utils/output.js.map +1 -0
  98. package/package.json +56 -0
@@ -0,0 +1,1073 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createMigrateCommand = createMigrateCommand;
40
+ const commander_1 = require("commander");
41
+ const ora_1 = __importDefault(require("ora"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const prompts_1 = require("@inquirer/prompts");
44
+ const api_1 = require("../lib/api");
45
+ const output = __importStar(require("../utils/output"));
46
+ function isRetryableError(error) {
47
+ if (error instanceof Error) {
48
+ const message = error.message.toLowerCase();
49
+ // Retry on network errors, timeouts, and rate limits only
50
+ if (message.includes("econnreset") ||
51
+ message.includes("econnrefused") ||
52
+ message.includes("etimedout") ||
53
+ message.includes("socket hang up") ||
54
+ message.includes("network") ||
55
+ message.includes("timeout") ||
56
+ message.includes("fetch failed")) {
57
+ return true;
58
+ }
59
+ }
60
+ // Retry on 429 (rate limit) and 5xx (transient server errors)
61
+ if (error && typeof error === "object" && "status" in error) {
62
+ const status = error.status;
63
+ if (status === 429 || (status >= 500 && status <= 599))
64
+ return true;
65
+ }
66
+ // Also check error message for HTTP status codes (e.g. LemonSqueezyClient throws
67
+ // `new Error(...)` with status embedded in the message, not as a property)
68
+ if (error instanceof Error) {
69
+ const msg = error.message;
70
+ if (/\b429\b/.test(msg) || /rate.?limit/i.test(msg))
71
+ return true;
72
+ if (/\b5\d{2}\b/.test(msg))
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ async function withRetry(fn, label, maxAttempts = 3, baseDelayMs = 1000) {
78
+ if (maxAttempts < 1) {
79
+ throw new Error("maxAttempts must be at least 1");
80
+ }
81
+ let lastError;
82
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
83
+ try {
84
+ return await fn();
85
+ }
86
+ catch (error) {
87
+ lastError = error instanceof Error ? error : new Error(String(error));
88
+ if (attempt < maxAttempts && isRetryableError(error)) {
89
+ const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
90
+ console.error(chalk_1.default.dim(` ↻ Attempt ${attempt + 1}/${maxAttempts} for ${label} in ${delayMs}ms...`));
91
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
92
+ }
93
+ else {
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ throw lastError;
99
+ }
100
+ // ============================================================================
101
+ // Lemon Squeezy API Client
102
+ // ============================================================================
103
+ class LemonSqueezyClient {
104
+ apiKey;
105
+ baseUrl = "https://api.lemonsqueezy.com/v1";
106
+ storeId;
107
+ constructor(apiKey) {
108
+ this.apiKey = apiKey;
109
+ }
110
+ setStoreId(storeId) {
111
+ this.storeId = storeId;
112
+ }
113
+ async request(endpoint, page = 1, perPage = 100, filterByStore = false) {
114
+ return withRetry(async () => {
115
+ const url = new URL(`${this.baseUrl}${endpoint}`);
116
+ url.searchParams.set("page[number]", page.toString());
117
+ url.searchParams.set("page[size]", perPage.toString());
118
+ // Filter by store_id to only fetch data from the validated store
119
+ if (filterByStore && this.storeId) {
120
+ url.searchParams.set("filter[store_id]", this.storeId.toString());
121
+ }
122
+ const response = await fetch(url.toString(), {
123
+ headers: {
124
+ Accept: "application/vnd.api+json",
125
+ "Content-Type": "application/vnd.api+json",
126
+ Authorization: `Bearer ${this.apiKey}`,
127
+ },
128
+ });
129
+ if (!response.ok) {
130
+ const errorText = await response.text();
131
+ throw new Error(`Lemon Squeezy API error (${response.status}): ${errorText}`);
132
+ }
133
+ return response.json();
134
+ }, `${endpoint} (page ${page})`);
135
+ }
136
+ async *paginate(endpoint, filterByStore = false) {
137
+ let page = 1;
138
+ let hasMore = true;
139
+ while (hasMore) {
140
+ const response = await this.request(endpoint, page, 100, filterByStore);
141
+ yield response.data;
142
+ // Check for both undefined and null - JSON:API spec allows null for unavailable links
143
+ hasMore = response.links.next != null;
144
+ page++;
145
+ // Rate limit: LS allows 300 requests per minute (5 req/sec)
146
+ // Using 200ms delay for safety margin
147
+ if (hasMore) {
148
+ await new Promise((resolve) => setTimeout(resolve, 200));
149
+ }
150
+ }
151
+ }
152
+ async fetchAll(endpoint, filterByStore = false) {
153
+ const items = [];
154
+ for await (const batch of this.paginate(endpoint, filterByStore)) {
155
+ items.push(...batch);
156
+ }
157
+ return items;
158
+ }
159
+ async getProducts() {
160
+ return this.fetchAll("/products", true);
161
+ }
162
+ async getVariants() {
163
+ // Variants endpoint doesn't support store_id filter - filter client-side via product_id
164
+ return this.fetchAll("/variants", false);
165
+ }
166
+ async getDiscounts() {
167
+ return this.fetchAll("/discounts", true);
168
+ }
169
+ async getCustomers() {
170
+ return this.fetchAll("/customers", true);
171
+ }
172
+ async getFiles() {
173
+ // Files endpoint doesn't support store_id filter - filtered in buildMigrationPlan via productMap
174
+ return this.fetchAll("/files", false);
175
+ }
176
+ async getStores() {
177
+ return this.fetchAll("/stores", false);
178
+ }
179
+ async validateKey() {
180
+ try {
181
+ const stores = await this.getStores();
182
+ if (stores.length === 0) {
183
+ return { valid: false, error: "No store found for this API key" };
184
+ }
185
+ return { valid: true, stores };
186
+ }
187
+ catch (error) {
188
+ return {
189
+ valid: false,
190
+ error: error instanceof Error ? error.message : "Unknown error",
191
+ };
192
+ }
193
+ }
194
+ }
195
+ function mapBillingPeriod(interval, intervalCount) {
196
+ // Missing interval for subscriptions should be skipped - can happen with deprecated LS variant fields
197
+ // This function is only called when is_subscription is true, so missing interval is invalid
198
+ if (!interval) {
199
+ return {
200
+ period: undefined,
201
+ warning: "Subscription variant missing billing interval (possibly deprecated LS field)",
202
+ skip: true,
203
+ };
204
+ }
205
+ // Map LS intervals to CREEM billing periods
206
+ if (interval === "month") {
207
+ // Handle undefined intervalCount as default monthly (same pattern as year branch)
208
+ if (intervalCount === 1 || intervalCount === undefined)
209
+ return { period: "every-month" };
210
+ if (intervalCount === 3)
211
+ return { period: "every-three-months" };
212
+ if (intervalCount === 6)
213
+ return { period: "every-six-months" };
214
+ // Unsupported month intervals (2, 4, 5, etc.) - skip with warning
215
+ return {
216
+ period: undefined,
217
+ warning: `${intervalCount}-month billing not supported in CREEM`,
218
+ skip: true,
219
+ };
220
+ }
221
+ if (interval === "year") {
222
+ if (intervalCount === 1 || intervalCount === undefined) {
223
+ return { period: "every-year" };
224
+ }
225
+ // Multi-year intervals not supported - skip to avoid pricing errors
226
+ return {
227
+ period: undefined,
228
+ warning: `${intervalCount}-year billing not supported in CREEM`,
229
+ skip: true,
230
+ };
231
+ }
232
+ // Day/week intervals not supported in CREEM - MUST skip to avoid wrong pricing
233
+ // A $5/week product silently becoming $5/month would be a 4x pricing error
234
+ return {
235
+ period: undefined,
236
+ warning: `${interval} billing interval not supported in CREEM (would cause incorrect pricing)`,
237
+ skip: true,
238
+ };
239
+ }
240
+ function mapDiscountType(amountType) {
241
+ return amountType === "percent" ? "percentage" : "fixed";
242
+ }
243
+ /**
244
+ * Map a Lemon Squeezy discount to CREEM discount format.
245
+ * Centralized helper to avoid duplication across skipped and active discount handling.
246
+ */
247
+ function mapLsDiscountToCreemDiscount(discount, storeCurrency) {
248
+ const creemDiscount = {
249
+ name: discount.attributes.name,
250
+ code: discount.attributes.code,
251
+ type: mapDiscountType(discount.attributes.amount_type),
252
+ duration: discount.attributes.duration,
253
+ durationInMonths: discount.attributes.duration_in_months,
254
+ maxRedemptions: discount.attributes.is_limited_redemptions
255
+ ? discount.attributes.max_redemptions
256
+ : undefined,
257
+ expiryDate: discount.attributes.expires_at
258
+ ? new Date(discount.attributes.expires_at)
259
+ : undefined,
260
+ };
261
+ // Set amount/percentage based on discount type
262
+ if (discount.attributes.amount_type === "percent") {
263
+ creemDiscount.percentage = discount.attributes.amount;
264
+ }
265
+ else {
266
+ // Fixed amount - LS stores in cents, CREEM also expects cents
267
+ creemDiscount.amount = discount.attributes.amount;
268
+ creemDiscount.currency = storeCurrency;
269
+ }
270
+ return creemDiscount;
271
+ }
272
+ /**
273
+ * Get the effective price for a variant.
274
+ * For PWYW (Pay What You Want) variants, the `price` field is deprecated and may be 0.
275
+ * Instead, use min_price (minimum acceptable) or suggested_price (recommended) or 0 if none set.
276
+ */
277
+ function getVariantPrice(variant) {
278
+ if (variant.attributes.pay_what_you_want) {
279
+ // For PWYW, prefer min_price (sets a floor), fall back to suggested_price, then 0
280
+ // Use || instead of ?? because min_price: 0 means "no minimum" in LS (should fall through)
281
+ return variant.attributes.min_price || variant.attributes.suggested_price || 0;
282
+ }
283
+ return variant.attributes.price;
284
+ }
285
+ /**
286
+ * Check if a variant is eligible for migration.
287
+ * Must match the same criteria used in the main migration loop.
288
+ */
289
+ function isVariantMigrateable(variant, product) {
290
+ if (!product)
291
+ return false;
292
+ // Import all products regardless of status (draft, pending, published)
293
+ // Users may want to migrate their entire catalog including work-in-progress
294
+ // Skip variants with no price (unless PWYW)
295
+ if (variant.attributes.price === 0 && !variant.attributes.pay_what_you_want) {
296
+ return false;
297
+ }
298
+ // For recurring variants, check if billing interval is supported
299
+ if (variant.attributes.is_subscription) {
300
+ const billingResult = mapBillingPeriod(variant.attributes.interval, variant.attributes.interval_count);
301
+ if (billingResult.skip) {
302
+ return false;
303
+ }
304
+ }
305
+ return true;
306
+ }
307
+ function buildMigrationPlan(products, variants, discounts, customers, files, storeCurrency) {
308
+ const productMap = new Map(products.map((p) => [parseInt(p.id, 10), p]));
309
+ // Pre-compute migrateable variant counts per product to avoid O(N²) complexity
310
+ const migrateableVariantCounts = new Map();
311
+ for (const variant of variants) {
312
+ const productId = variant.attributes.product_id;
313
+ const product = productMap.get(productId);
314
+ if (isVariantMigrateable(variant, product)) {
315
+ migrateableVariantCounts.set(productId, (migrateableVariantCounts.get(productId) || 0) + 1);
316
+ }
317
+ }
318
+ // Build products from variants
319
+ // Uses isVariantMigrateable as the single source of truth for eligibility checks
320
+ const productPlan = [];
321
+ for (const variant of variants) {
322
+ const product = productMap.get(variant.attributes.product_id);
323
+ if (!product)
324
+ continue;
325
+ const isRecurring = variant.attributes.is_subscription;
326
+ const billingResult = isRecurring
327
+ ? mapBillingPeriod(variant.attributes.interval, variant.attributes.interval_count)
328
+ : { period: undefined };
329
+ // Use pre-computed count for naming decision
330
+ const productId = parseInt(product.id, 10);
331
+ const migrateableVariantCount = migrateableVariantCounts.get(productId) || 0;
332
+ const productName = migrateableVariantCount > 1
333
+ ? `${product.attributes.name} - ${variant.attributes.name}`
334
+ : product.attributes.name;
335
+ // Use isVariantMigrateable for all eligibility checks
336
+ if (!isVariantMigrateable(variant, product)) {
337
+ // Determine the specific reason for skipping - ALWAYS track and report
338
+ let skipReason;
339
+ const hasPrice = variant.attributes.price > 0 || variant.attributes.pay_what_you_want;
340
+ if (!hasPrice) {
341
+ skipReason = "Variant has no price set";
342
+ }
343
+ else if (billingResult.skip && billingResult.warning) {
344
+ skipReason = billingResult.warning;
345
+ }
346
+ else {
347
+ skipReason = "Unknown eligibility issue";
348
+ }
349
+ productPlan.push({
350
+ lsProduct: product,
351
+ lsVariant: variant,
352
+ creemProduct: {
353
+ name: productName,
354
+ description: product.attributes.description || "",
355
+ price: getVariantPrice(variant),
356
+ currency: storeCurrency,
357
+ billingType: isRecurring ? "recurring" : "onetime",
358
+ billingPeriod: billingResult.period,
359
+ taxCategory: "saas",
360
+ },
361
+ skipped: true,
362
+ skipReason,
363
+ });
364
+ continue;
365
+ }
366
+ productPlan.push({
367
+ lsProduct: product,
368
+ lsVariant: variant,
369
+ creemProduct: {
370
+ name: productName,
371
+ description: product.attributes.description || "",
372
+ price: getVariantPrice(variant), // Use min_price/suggested_price for PWYW variants
373
+ currency: storeCurrency, // Use store's configured currency (LS supports 150+ currencies)
374
+ billingType: isRecurring ? "recurring" : "onetime",
375
+ billingPeriod: billingResult.period,
376
+ taxCategory: "saas",
377
+ },
378
+ });
379
+ }
380
+ // Build discounts
381
+ const discountPlan = [];
382
+ for (const discount of discounts) {
383
+ // Skip expired or inactive discounts
384
+ if (discount.attributes.status !== "published") {
385
+ discountPlan.push({
386
+ lsDiscount: discount,
387
+ creemDiscount: mapLsDiscountToCreemDiscount(discount, storeCurrency),
388
+ skipped: true,
389
+ skipReason: `Discount status is "${discount.attributes.status}" (not published)`,
390
+ });
391
+ continue;
392
+ }
393
+ // Skip product-scoped discounts - LS API doesn't expose which products they apply to
394
+ // Migrating them would apply to ALL products, potentially causing revenue loss
395
+ if (discount.attributes.is_limited_to_products) {
396
+ discountPlan.push({
397
+ lsDiscount: discount,
398
+ creemDiscount: mapLsDiscountToCreemDiscount(discount, storeCurrency),
399
+ skipped: true,
400
+ skipReason: "Product-scoped discount (requires manual setup in CREEM to avoid applying to wrong products)",
401
+ });
402
+ continue;
403
+ }
404
+ // Map LS discount to CREEM format using centralized helper
405
+ const creemDiscount = mapLsDiscountToCreemDiscount(discount, storeCurrency);
406
+ discountPlan.push({
407
+ lsDiscount: discount,
408
+ creemDiscount,
409
+ });
410
+ }
411
+ // Build customers
412
+ const customerPlan = [];
413
+ for (const customer of customers) {
414
+ customerPlan.push({
415
+ lsCustomer: customer,
416
+ creemCustomer: {
417
+ email: customer.attributes.email,
418
+ name: customer.attributes.name,
419
+ billingAddress: customer.attributes.city || customer.attributes.country
420
+ ? {
421
+ city: customer.attributes.city,
422
+ country: customer.attributes.country,
423
+ }
424
+ : undefined,
425
+ },
426
+ });
427
+ }
428
+ // Build files (for later reference)
429
+ // Filter to only include files belonging to products in our store (via productMap)
430
+ const filePlan = [];
431
+ for (const file of files) {
432
+ const variant = variants.find((v) => parseInt(v.id, 10) === file.attributes.variant_id);
433
+ const product = variant ? productMap.get(variant.attributes.product_id) : undefined;
434
+ // Skip files that don't belong to products in our store
435
+ if (!product)
436
+ continue;
437
+ filePlan.push({
438
+ lsFile: file,
439
+ variantId: file.attributes.variant_id.toString(),
440
+ productName: product.attributes.name,
441
+ });
442
+ }
443
+ const migrateableProducts = productPlan.filter((p) => !p.skipped);
444
+ const skippedProducts = productPlan.filter((p) => p.skipped);
445
+ const migrateableDiscounts = discountPlan.filter((d) => !d.skipped);
446
+ const skippedDiscounts = discountPlan.filter((d) => d.skipped);
447
+ return {
448
+ products: productPlan,
449
+ discounts: discountPlan,
450
+ customers: customerPlan,
451
+ files: filePlan,
452
+ summary: {
453
+ totalProducts: migrateableProducts.length,
454
+ totalDiscounts: migrateableDiscounts.length,
455
+ totalCustomers: customerPlan.length,
456
+ totalFiles: filePlan.length,
457
+ skippedProducts: skippedProducts.length,
458
+ skippedDiscounts: skippedDiscounts.length,
459
+ },
460
+ };
461
+ }
462
+ // ============================================================================
463
+ // Migration Execution
464
+ // ============================================================================
465
+ async function executeMigration(plan, options) {
466
+ const result = {
467
+ success: true,
468
+ created: { products: [], discounts: [], customers: [], files: [] },
469
+ failed: { products: [], discounts: [], customers: [], files: [] },
470
+ skipped: { products: [], discounts: [], customers: [] },
471
+ };
472
+ if (options.dryRun) {
473
+ console.log();
474
+ console.log(chalk_1.default.yellow("DRY RUN MODE - No changes will be made"));
475
+ console.log();
476
+ return result;
477
+ }
478
+ const client = (0, api_1.getClient)();
479
+ // Migrate Products (skip products with unsupported billing intervals)
480
+ const migrateableProducts = plan.products.filter((p) => !p.skipped);
481
+ const skippedProducts = plan.products.filter((p) => p.skipped);
482
+ console.log();
483
+ console.log(chalk_1.default.bold("Migrating Products..."));
484
+ // Record skipped products first
485
+ for (const skipped of skippedProducts) {
486
+ result.skipped.products.push({
487
+ name: skipped.creemProduct.name,
488
+ reason: skipped.skipReason || "Unsupported billing interval",
489
+ });
490
+ }
491
+ for (let i = 0; i < migrateableProducts.length; i++) {
492
+ const item = migrateableProducts[i];
493
+ const progress = `[${i + 1}/${migrateableProducts.length}]`;
494
+ const spinner = (0, ora_1.default)(`${progress} Creating: ${item.creemProduct.name}`).start();
495
+ try {
496
+ const params = {
497
+ name: item.creemProduct.name,
498
+ // CREEM API requires description to be a non-empty string
499
+ // Use product name as fallback if no description is provided
500
+ description: item.creemProduct.description || item.creemProduct.name,
501
+ price: item.creemProduct.price,
502
+ currency: item.creemProduct.currency,
503
+ billingType: item.creemProduct.billingType,
504
+ taxCategory: item.creemProduct.taxCategory,
505
+ };
506
+ if (item.creemProduct.billingPeriod) {
507
+ params.billingPeriod = item.creemProduct.billingPeriod;
508
+ }
509
+ // NOTE: Retrying a POST create is not idempotent — a transient failure after the server
510
+ // processes the request could lead to a duplicate product. This is an acceptable tradeoff:
511
+ // duplicates can be cleaned up manually, whereas failing the entire migration is worse.
512
+ const created = (await withRetry(() => client.products.create(params), `product "${item.creemProduct.name}"`));
513
+ result.created.products.push(created.id);
514
+ spinner.succeed(`${progress} Created: ${item.creemProduct.name}`);
515
+ }
516
+ catch (error) {
517
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
518
+ result.failed.products.push({
519
+ name: item.creemProduct.name,
520
+ error: errorMsg,
521
+ });
522
+ spinner.fail(`${progress} Failed: ${item.creemProduct.name} - ${errorMsg}`);
523
+ result.success = false;
524
+ }
525
+ // Small delay to avoid rate limiting
526
+ await new Promise((resolve) => setTimeout(resolve, 200));
527
+ }
528
+ // Migrate Discounts (after products so we can apply to all created products)
529
+ const migrateableDiscounts = plan.discounts.filter((d) => !d.skipped);
530
+ const skippedDiscounts = plan.discounts.filter((d) => d.skipped);
531
+ if (plan.discounts.length > 0) {
532
+ console.log();
533
+ console.log(chalk_1.default.bold("Migrating Discounts..."));
534
+ // Record skipped discounts first
535
+ for (const skipped of skippedDiscounts) {
536
+ result.skipped.discounts.push({
537
+ code: skipped.creemDiscount.code,
538
+ reason: skipped.skipReason || "Discount not active",
539
+ });
540
+ }
541
+ // Skip discount creation if no products were successfully created
542
+ // CREEM API requires at least one product for discounts to apply to
543
+ if (result.created.products.length === 0 && migrateableDiscounts.length > 0) {
544
+ console.log(chalk_1.default.yellow(` ⚠ Skipping ${migrateableDiscounts.length} discount(s): No products were created to apply them to`));
545
+ for (const item of migrateableDiscounts) {
546
+ result.skipped.discounts.push({
547
+ code: item.creemDiscount.code,
548
+ reason: "No products available to apply discount to",
549
+ });
550
+ }
551
+ }
552
+ // Create discounts - apply to all created products
553
+ for (let i = 0; i < (result.created.products.length > 0 ? migrateableDiscounts.length : 0); i++) {
554
+ const item = migrateableDiscounts[i];
555
+ const progress = `[${i + 1}/${migrateableDiscounts.length}]`;
556
+ const spinner = (0, ora_1.default)(`${progress} Creating: ${item.creemDiscount.code}`).start();
557
+ try {
558
+ // Build discount creation params
559
+ const params = {
560
+ name: item.creemDiscount.name,
561
+ code: item.creemDiscount.code,
562
+ type: item.creemDiscount.type,
563
+ duration: item.creemDiscount.duration,
564
+ // Apply to all created products from this migration
565
+ appliesToProducts: result.created.products,
566
+ };
567
+ // Set amount/percentage based on type
568
+ if (item.creemDiscount.type === "percentage" &&
569
+ item.creemDiscount.percentage !== undefined) {
570
+ params.percentage = item.creemDiscount.percentage;
571
+ }
572
+ else if (item.creemDiscount.type === "fixed" && item.creemDiscount.amount !== undefined) {
573
+ params.amount = item.creemDiscount.amount;
574
+ params.currency = item.creemDiscount.currency;
575
+ }
576
+ // Optional fields
577
+ if (item.creemDiscount.expiryDate) {
578
+ params.expiryDate = item.creemDiscount.expiryDate;
579
+ }
580
+ if (item.creemDiscount.maxRedemptions !== undefined) {
581
+ params.maxRedemptions = item.creemDiscount.maxRedemptions;
582
+ }
583
+ if (item.creemDiscount.duration === "repeating" &&
584
+ item.creemDiscount.durationInMonths !== undefined) {
585
+ params.durationInMonths = item.creemDiscount.durationInMonths;
586
+ }
587
+ // NOTE: Retrying a POST create is not idempotent — see product create comment above.
588
+ // Discount codes are unique, so duplicates will fail with a conflict error rather than
589
+ // silently creating duplicates.
590
+ const created = (await withRetry(() => client.discounts.create(params), `discount "${item.creemDiscount.code}"`));
591
+ result.created.discounts.push(created.id);
592
+ spinner.succeed(`${progress} Created: ${item.creemDiscount.code}`);
593
+ }
594
+ catch (error) {
595
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
596
+ result.failed.discounts.push({
597
+ code: item.creemDiscount.code,
598
+ error: errorMsg,
599
+ });
600
+ spinner.fail(`${progress} Failed: ${item.creemDiscount.code} - ${errorMsg}`);
601
+ result.success = false;
602
+ }
603
+ // Small delay to avoid rate limiting
604
+ await new Promise((resolve) => setTimeout(resolve, 200));
605
+ }
606
+ }
607
+ // Note: Customer and File migration require additional CREEM API endpoints
608
+ // that are not yet available in the SDK. Logging for manual follow-up.
609
+ if (plan.customers.length > 0) {
610
+ console.log();
611
+ console.log(chalk_1.default.yellow(`⚠ ${plan.customers.length} customers found - will be created automatically on first purchase`));
612
+ console.log(chalk_1.default.dim(" Customers are auto-created in CREEM when they make their first purchase"));
613
+ }
614
+ if (plan.files.length > 0) {
615
+ console.log();
616
+ console.log(chalk_1.default.yellow(`⚠ ${plan.files.length} files found - manual upload required`));
617
+ console.log(chalk_1.default.dim(" Upload downloadable files in the CREEM dashboard"));
618
+ }
619
+ return result;
620
+ }
621
+ // ============================================================================
622
+ // CLI Commands
623
+ // ============================================================================
624
+ function createLemonSqueezyCommand() {
625
+ const command = new commander_1.Command("lemon-squeezy")
626
+ .description("Migrate from Lemon Squeezy to CREEM")
627
+ .option("--ls-api-key <key>", "Lemon Squeezy API key")
628
+ .option("--ls-store-id <id>", "Lemon Squeezy store ID to migrate from")
629
+ .option("--dry-run", "Preview migration without making changes")
630
+ // Note: --log-file is not yet implemented - will be added in future version
631
+ .option("--json", "Output migration plan as JSON (implies --dry-run)")
632
+ .option("--exclude-discounts", "Skip migrating discounts entirely")
633
+ .action(async (options) => {
634
+ // Skip banner when outputting JSON to avoid corrupting stdout
635
+ if (!options.json) {
636
+ console.log();
637
+ console.log(chalk_1.default.bold.cyan("🍋 Lemon Squeezy → CREEM Migration"));
638
+ console.log(chalk_1.default.dim("Migrate your products, customers, and discounts"));
639
+ console.log();
640
+ }
641
+ // Verify CREEM authentication
642
+ try {
643
+ (0, api_1.getClient)();
644
+ }
645
+ catch {
646
+ output.error("Not authenticated with CREEM. Run `creem login` first.");
647
+ process.exit(1);
648
+ }
649
+ // Get Lemon Squeezy API key
650
+ let lsApiKey = options.lsApiKey;
651
+ if (!lsApiKey) {
652
+ try {
653
+ lsApiKey = await (0, prompts_1.password)({
654
+ message: "Enter your Lemon Squeezy API key:",
655
+ validate: (value) => (value.length > 0 ? true : "API key is required"),
656
+ });
657
+ }
658
+ catch {
659
+ console.log(chalk_1.default.dim("\nMigration cancelled."));
660
+ return;
661
+ }
662
+ if (!lsApiKey) {
663
+ console.log(chalk_1.default.dim("\nMigration cancelled."));
664
+ return;
665
+ }
666
+ }
667
+ // Validate Lemon Squeezy API key
668
+ const lsClient = new LemonSqueezyClient(lsApiKey);
669
+ const validateSpinner = (0, ora_1.default)("Validating Lemon Squeezy API key...").start();
670
+ const validation = await lsClient.validateKey();
671
+ if (!validation.valid) {
672
+ validateSpinner.fail("Invalid Lemon Squeezy API key");
673
+ output.error(validation.error || "Could not validate API key");
674
+ process.exit(1);
675
+ }
676
+ const stores = validation.stores || [];
677
+ validateSpinner.succeed(`Connected to Lemon Squeezy (${stores.length} store${stores.length === 1 ? "" : "s"} found)`);
678
+ let selectedStore;
679
+ if (options.lsStoreId) {
680
+ if (!/^\d+$/.test(options.lsStoreId)) {
681
+ output.error(`Invalid Lemon Squeezy store ID: ${options.lsStoreId}`);
682
+ process.exit(1);
683
+ }
684
+ const requestedStoreId = parseInt(options.lsStoreId, 10);
685
+ if (!Number.isInteger(requestedStoreId) || requestedStoreId <= 0) {
686
+ output.error(`Invalid Lemon Squeezy store ID: ${options.lsStoreId}`);
687
+ process.exit(1);
688
+ }
689
+ selectedStore = stores.find((store) => parseInt(store.id, 10) === requestedStoreId);
690
+ if (!selectedStore) {
691
+ output.error(`Lemon Squeezy store ID ${options.lsStoreId} was not found for this API key.`);
692
+ if (stores.length > 0) {
693
+ output.dim(`Available store IDs: ${stores.map((store) => store.id).join(", ")}`);
694
+ }
695
+ process.exit(1);
696
+ }
697
+ }
698
+ else if (stores.length === 1) {
699
+ selectedStore = stores[0];
700
+ }
701
+ else {
702
+ try {
703
+ selectedStore = await (0, prompts_1.select)({
704
+ message: "Which Lemon Squeezy store do you want to migrate?",
705
+ choices: stores.map((store) => {
706
+ const parts = [
707
+ store.attributes.name,
708
+ store.attributes.domain || store.attributes.slug,
709
+ `ID: ${store.id}`,
710
+ store.attributes.currency,
711
+ ].filter(Boolean);
712
+ return {
713
+ name: parts.join(" · "),
714
+ value: store,
715
+ };
716
+ }),
717
+ });
718
+ }
719
+ catch {
720
+ console.log(chalk_1.default.dim("\nMigration cancelled."));
721
+ return;
722
+ }
723
+ }
724
+ if (!selectedStore) {
725
+ output.error("No Lemon Squeezy store selected. Cannot proceed with migration.");
726
+ process.exit(1);
727
+ }
728
+ const selectedStoreId = parseInt(selectedStore.id, 10);
729
+ if (!Number.isInteger(selectedStoreId) || selectedStoreId <= 0) {
730
+ output.error(`Invalid Lemon Squeezy store ID: ${selectedStore.id}`);
731
+ process.exit(1);
732
+ }
733
+ const selectedStoreCurrency = selectedStore.attributes?.currency;
734
+ if (!selectedStoreCurrency) {
735
+ output.error("Store currency not found. Cannot proceed with migration.");
736
+ process.exit(1);
737
+ }
738
+ if (!options.json) {
739
+ console.log(chalk_1.default.dim(`Using Lemon Squeezy store: ${selectedStore.attributes.name} (ID: ${selectedStore.id})`));
740
+ }
741
+ // Set store ID for filtering subsequent API requests
742
+ lsClient.setStoreId(selectedStoreId);
743
+ // Validate store currency - CREEM only supports USD and EUR
744
+ const rawCurrency = selectedStoreCurrency.toUpperCase();
745
+ if (rawCurrency !== "USD" && rawCurrency !== "EUR") {
746
+ output.error(`Your Lemon Squeezy store uses ${rawCurrency}, but CREEM currently only supports USD and EUR.\n` +
747
+ `To migrate, please either:\n` +
748
+ ` 1. Change your Lemon Squeezy store currency to USD or EUR before migration\n` +
749
+ ` 2. Contact CREEM support for assistance with currency conversion\n` +
750
+ `\nNote: Migrating with incorrect currency would result in wrong pricing in CREEM.`);
751
+ process.exit(1);
752
+ }
753
+ const storeCurrency = rawCurrency;
754
+ // Fetch data from Lemon Squeezy
755
+ const fetchSpinner = (0, ora_1.default)("Fetching data from Lemon Squeezy...").start();
756
+ let products = [];
757
+ let variants = [];
758
+ let discounts = [];
759
+ let customers = [];
760
+ let files = [];
761
+ const fetchFailures = [];
762
+ fetchSpinner.text = "Fetching products...";
763
+ try {
764
+ products = await lsClient.getProducts();
765
+ }
766
+ catch (error) {
767
+ const msg = error instanceof Error ? error.message : "Unknown error";
768
+ fetchFailures.push({
769
+ type: "product",
770
+ label: "all products",
771
+ error: msg,
772
+ });
773
+ fetchSpinner.text = "Products fetch failed, continuing...";
774
+ }
775
+ fetchSpinner.text = "Fetching variants...";
776
+ try {
777
+ variants = await lsClient.getVariants();
778
+ }
779
+ catch (error) {
780
+ const msg = error instanceof Error ? error.message : "Unknown error";
781
+ fetchFailures.push({
782
+ type: "variant",
783
+ label: "all variants",
784
+ error: msg,
785
+ });
786
+ fetchSpinner.text = "Variants fetch failed, continuing...";
787
+ }
788
+ if (options.excludeDiscounts) {
789
+ discounts = [];
790
+ }
791
+ else {
792
+ fetchSpinner.text = "Fetching discounts...";
793
+ try {
794
+ discounts = await lsClient.getDiscounts();
795
+ }
796
+ catch (error) {
797
+ const msg = error instanceof Error ? error.message : "Unknown error";
798
+ fetchFailures.push({
799
+ type: "discount",
800
+ label: "all discounts",
801
+ error: msg,
802
+ });
803
+ fetchSpinner.text = "Discounts fetch failed, continuing...";
804
+ }
805
+ }
806
+ fetchSpinner.text = "Fetching customers...";
807
+ try {
808
+ customers = await lsClient.getCustomers();
809
+ }
810
+ catch (error) {
811
+ const msg = error instanceof Error ? error.message : "Unknown error";
812
+ fetchFailures.push({
813
+ type: "customer",
814
+ label: "all customers",
815
+ error: msg,
816
+ });
817
+ fetchSpinner.text = "Customers fetch failed, continuing...";
818
+ }
819
+ fetchSpinner.text = "Fetching files...";
820
+ try {
821
+ files = await lsClient.getFiles();
822
+ }
823
+ catch (error) {
824
+ const msg = error instanceof Error ? error.message : "Unknown error";
825
+ fetchFailures.push({ type: "file", label: "all files", error: msg });
826
+ fetchSpinner.text = "Files fetch failed, continuing...";
827
+ }
828
+ {
829
+ const discountsLabel = options.excludeDiscounts
830
+ ? "discounts excluded"
831
+ : `${discounts.length} discounts`;
832
+ const failLabel = fetchFailures.length > 0
833
+ ? chalk_1.default.yellow(` (${fetchFailures.length} fetch failures)`)
834
+ : "";
835
+ fetchSpinner.succeed(`Fetched: ${products.length} products, ${variants.length} variants, ${discountsLabel}, ${customers.length} customers, ${files.length} files${failLabel}`);
836
+ }
837
+ if (fetchFailures.length > 0 && !options.json) {
838
+ console.error();
839
+ console.error(chalk_1.default.yellow("⚠ Some data could not be fetched after retries:"));
840
+ for (const f of fetchFailures) {
841
+ console.error(` • ${f.type}: ${f.label} - ${f.error}`);
842
+ }
843
+ console.error(chalk_1.default.dim(" Migration will continue with the data that was successfully fetched."));
844
+ }
845
+ // Build migration plan (storeCurrency already validated as USD or EUR above)
846
+ const planSpinner = (0, ora_1.default)("Building migration plan...").start();
847
+ const plan = buildMigrationPlan(products, variants, discounts, customers, files, storeCurrency);
848
+ planSpinner.succeed("Migration plan ready");
849
+ // If JSON output requested, print and exit
850
+ if (options.json) {
851
+ const jsonOutput = fetchFailures.length > 0 ? { ...plan, fetchFailures } : plan;
852
+ output.outputJson(jsonOutput);
853
+ return;
854
+ }
855
+ // Display migration summary
856
+ console.log();
857
+ console.log(chalk_1.default.bold("Migration Summary"));
858
+ console.log(chalk_1.default.dim("─".repeat(50)));
859
+ console.log(` Products: ${chalk_1.default.cyan(plan.summary.totalProducts)} CREEM products from ${variants.length} LS variants`);
860
+ if (plan.summary.skippedProducts > 0) {
861
+ console.log(` Skipped: ${chalk_1.default.yellow(plan.summary.skippedProducts)} variants (see details below)`);
862
+ }
863
+ if (options.excludeDiscounts) {
864
+ console.log(` Discounts: ${chalk_1.default.yellow("excluded")} (--exclude-discounts)`);
865
+ }
866
+ else {
867
+ console.log(` Discounts: ${chalk_1.default.cyan(plan.summary.totalDiscounts)}${plan.summary.skippedDiscounts > 0 ? chalk_1.default.yellow(` (${plan.summary.skippedDiscounts} skipped)`) : ""}`);
868
+ }
869
+ console.log(` Customers: ${chalk_1.default.cyan(plan.summary.totalCustomers)}`);
870
+ console.log(` Files: ${chalk_1.default.cyan(plan.summary.totalFiles)} (manual upload required)`);
871
+ console.log(chalk_1.default.dim("─".repeat(50)));
872
+ if (plan.summary.totalProducts === 0 && plan.summary.totalCustomers === 0) {
873
+ console.log();
874
+ if (plan.summary.skippedProducts > 0) {
875
+ // All products were skipped - show why
876
+ console.log(chalk_1.default.yellow(`No products can be migrated. ${plan.summary.skippedProducts} variant(s) were skipped:`));
877
+ console.log();
878
+ const skippedProductsList = plan.products.filter((p) => p.skipped);
879
+ // Group by skip reason for cleaner output
880
+ const reasonCounts = new Map();
881
+ for (const p of skippedProductsList) {
882
+ const reason = p.skipReason || "Unknown reason";
883
+ reasonCounts.set(reason, (reasonCounts.get(reason) || 0) + 1);
884
+ }
885
+ console.log(chalk_1.default.dim("Skip reasons:"));
886
+ for (const [reason, count] of reasonCounts) {
887
+ console.log(` • ${chalk_1.default.yellow(count)} variant(s): ${reason}`);
888
+ }
889
+ console.log();
890
+ console.log(chalk_1.default.dim("Products may be skipped due to unsupported billing intervals or pricing."));
891
+ console.log(chalk_1.default.dim("CREEM supports one-time, monthly, and yearly billing cycles."));
892
+ }
893
+ else {
894
+ console.log(chalk_1.default.yellow("Nothing to migrate. Your Lemon Squeezy account appears empty."));
895
+ }
896
+ return;
897
+ }
898
+ // Show sample products (only migrateable ones)
899
+ const migrateableProducts = plan.products.filter((p) => !p.skipped);
900
+ const skippedProductsList = plan.products.filter((p) => p.skipped);
901
+ if (migrateableProducts.length > 0) {
902
+ console.log();
903
+ console.log(chalk_1.default.dim("Sample products to be created:"));
904
+ const sample = migrateableProducts.slice(0, 5);
905
+ for (const p of sample) {
906
+ const price = output.formatCurrency(p.creemProduct.price, p.creemProduct.currency);
907
+ const billing = p.creemProduct.billingType === "recurring"
908
+ ? `Recurring / ${p.creemProduct.billingPeriod?.replace("every-", "")}`
909
+ : "One-time";
910
+ console.log(` • ${p.creemProduct.name} - ${price} (${billing})`);
911
+ }
912
+ if (migrateableProducts.length > 5) {
913
+ console.log(chalk_1.default.dim(` ... and ${migrateableProducts.length - 5} more`));
914
+ }
915
+ }
916
+ // Show skipped products with reasons
917
+ if (skippedProductsList.length > 0) {
918
+ console.log();
919
+ console.log(chalk_1.default.yellow(`⚠ ${skippedProductsList.length} variant(s) cannot be migrated:`));
920
+ // Group by skip reason for cleaner output
921
+ const reasonCounts = new Map();
922
+ for (const p of skippedProductsList) {
923
+ const reason = p.skipReason || "Unknown reason";
924
+ if (!reasonCounts.has(reason)) {
925
+ reasonCounts.set(reason, []);
926
+ }
927
+ reasonCounts.get(reason).push(p.creemProduct.name);
928
+ }
929
+ for (const [reason, names] of reasonCounts) {
930
+ console.log(` • ${reason}: ${chalk_1.default.dim(`${names.length} variant(s)`)}`);
931
+ // Show first 2 names as examples
932
+ for (const name of names.slice(0, 2)) {
933
+ console.log(chalk_1.default.dim(` - ${name}`));
934
+ }
935
+ if (names.length > 2) {
936
+ console.log(chalk_1.default.dim(` ... and ${names.length - 2} more`));
937
+ }
938
+ }
939
+ }
940
+ // Show skipped discounts with reasons
941
+ const skippedDiscountsList = plan.discounts.filter((d) => d.skipped);
942
+ if (skippedDiscountsList.length > 0) {
943
+ console.log();
944
+ console.log(chalk_1.default.yellow(`⚠ ${skippedDiscountsList.length} discount(s) require manual setup:`));
945
+ // Group by skip reason for cleaner output
946
+ const discountReasonCounts = new Map();
947
+ for (const d of skippedDiscountsList) {
948
+ const reason = d.skipReason || "Unknown reason";
949
+ if (!discountReasonCounts.has(reason)) {
950
+ discountReasonCounts.set(reason, []);
951
+ }
952
+ discountReasonCounts.get(reason).push(d.creemDiscount.code);
953
+ }
954
+ for (const [reason, codes] of discountReasonCounts) {
955
+ console.log(` • ${reason}: ${chalk_1.default.dim(`${codes.length} discount(s)`)}`);
956
+ // Show first 3 codes as examples
957
+ for (const code of codes.slice(0, 3)) {
958
+ console.log(chalk_1.default.dim(` - ${code}`));
959
+ }
960
+ if (codes.length > 3) {
961
+ console.log(chalk_1.default.dim(` ... and ${codes.length - 3} more`));
962
+ }
963
+ }
964
+ }
965
+ // Dry run mode
966
+ if (options.dryRun) {
967
+ console.log();
968
+ console.log(chalk_1.default.yellow.bold("DRY RUN MODE"));
969
+ console.log(chalk_1.default.dim("No changes were made. Remove --dry-run to execute migration."));
970
+ return;
971
+ }
972
+ // Confirmation prompt
973
+ console.log();
974
+ let proceed;
975
+ try {
976
+ proceed = await (0, prompts_1.confirm)({
977
+ message: "Proceed with migration?",
978
+ default: false,
979
+ });
980
+ }
981
+ catch {
982
+ console.log(chalk_1.default.dim("\nMigration cancelled."));
983
+ return;
984
+ }
985
+ if (!proceed) {
986
+ console.log(chalk_1.default.dim("\nMigration cancelled."));
987
+ return;
988
+ }
989
+ // Execute migration
990
+ console.log();
991
+ console.log(chalk_1.default.bold.green("Starting migration..."));
992
+ const result = await executeMigration(plan, {
993
+ dryRun: false,
994
+ });
995
+ // Display results
996
+ console.log();
997
+ console.log(chalk_1.default.bold("Migration Complete"));
998
+ console.log(chalk_1.default.dim("─".repeat(50)));
999
+ console.log(` Products created: ${chalk_1.default.green(result.created.products.length)}`);
1000
+ console.log(` Discounts created: ${chalk_1.default.green(result.created.discounts.length)}`);
1001
+ if (result.failed.products.length > 0 || result.failed.discounts.length > 0) {
1002
+ console.log();
1003
+ console.log(chalk_1.default.red("Failed items:"));
1004
+ for (const f of result.failed.products) {
1005
+ console.log(` • Product: ${f.name} - ${f.error}`);
1006
+ }
1007
+ for (const f of result.failed.discounts) {
1008
+ console.log(` • Discount: ${f.code} - ${f.error}`);
1009
+ }
1010
+ }
1011
+ if (result.skipped.discounts.length > 0) {
1012
+ console.log();
1013
+ console.log(chalk_1.default.yellow("Skipped discounts:"));
1014
+ for (const s of result.skipped.discounts.slice(0, 5)) {
1015
+ console.log(` • ${s.code}: ${s.reason}`);
1016
+ }
1017
+ if (result.skipped.discounts.length > 5) {
1018
+ console.log(chalk_1.default.dim(` ... and ${result.skipped.discounts.length - 5} more`));
1019
+ }
1020
+ }
1021
+ console.log(chalk_1.default.dim("─".repeat(50)));
1022
+ if (fetchFailures.length > 0) {
1023
+ console.error();
1024
+ console.error(chalk_1.default.red("Failed to fetch (after retries):"));
1025
+ for (const f of fetchFailures) {
1026
+ console.error(` • ${f.type}: ${f.label} - ${f.error}`);
1027
+ }
1028
+ }
1029
+ if (result.success && fetchFailures.length === 0) {
1030
+ console.log();
1031
+ console.log(chalk_1.default.green.bold("✓ Migration completed successfully!"));
1032
+ }
1033
+ else {
1034
+ console.log();
1035
+ console.log(chalk_1.default.yellow.bold("⚠ Migration completed with some errors."));
1036
+ if (fetchFailures.length > 0) {
1037
+ console.log(chalk_1.default.dim(" Re-run the migration to retry failed fetches."));
1038
+ }
1039
+ }
1040
+ console.log();
1041
+ console.log(chalk_1.default.dim("Next steps:"));
1042
+ console.log(chalk_1.default.dim(" 1. Review your products at https://creem.io/dashboard/products"));
1043
+ console.log(chalk_1.default.dim(" 2. Review your discounts at https://creem.io/dashboard/discounts"));
1044
+ console.log(chalk_1.default.dim(" 3. Upload downloadable files in the dashboard if needed"));
1045
+ console.log(chalk_1.default.dim(" 4. Update your checkout links to use CREEM"));
1046
+ });
1047
+ return command;
1048
+ }
1049
+ function createMigrateCommand() {
1050
+ const command = new commander_1.Command("migrate")
1051
+ .description("Migrate from other platforms to CREEM")
1052
+ .addHelpText("after", `
1053
+ ${chalk_1.default.dim("Available migrations:")}
1054
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy")} Migrate from Lemon Squeezy
1055
+
1056
+ ${chalk_1.default.dim("Options:")}
1057
+ --dry-run Preview what would be migrated without making changes
1058
+ --ls-api-key <key> Provide API key via flag (skips interactive prompt)
1059
+ --ls-store-id <id> Lemon Squeezy store ID to migrate from
1060
+ --json Output migration plan as JSON
1061
+ --exclude-discounts Skip migrating discounts entirely
1062
+
1063
+ ${chalk_1.default.dim("Examples:")}
1064
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy")} Interactive migration wizard
1065
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy --dry-run")} Preview migration plan
1066
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy --ls-store-id 123")} Skip store selection prompt
1067
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy --ls-api-key ls_xxx --ls-store-id 123")} Non-interactive migration
1068
+ ${chalk_1.default.cyan("creem migrate lemon-squeezy --json > plan.json")} Export migration plan
1069
+ `);
1070
+ command.addCommand(createLemonSqueezyCommand());
1071
+ return command;
1072
+ }
1073
+ //# sourceMappingURL=migrate.js.map