@revstackhq/cli 0.0.0-dev-20260227103607 → 0.0.0-dev-20260228060138

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 (56) hide show
  1. package/.turbo/turbo-build.log +51 -42
  2. package/CHANGELOG.md +11 -0
  3. package/dist/cli.js +622 -49
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/init.d.ts +0 -7
  6. package/dist/commands/init.js +443 -24
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/login.d.ts +0 -6
  9. package/dist/commands/login.js.map +1 -1
  10. package/dist/commands/logout.d.ts +0 -5
  11. package/dist/commands/logout.js.map +1 -1
  12. package/dist/commands/pull.d.ts +0 -7
  13. package/dist/commands/pull.js +120 -11
  14. package/dist/commands/pull.js.map +1 -1
  15. package/dist/commands/push.js +59 -14
  16. package/dist/commands/push.js.map +1 -1
  17. package/dist/commands/templates/ai-agent-platform.d.ts +5 -0
  18. package/dist/commands/templates/ai-agent-platform.js +119 -0
  19. package/dist/commands/templates/ai-agent-platform.js.map +1 -0
  20. package/dist/commands/templates/b2b-saas.js +23 -10
  21. package/dist/commands/templates/b2b-saas.js.map +1 -1
  22. package/dist/commands/templates/developer-tools.d.ts +5 -0
  23. package/dist/commands/templates/developer-tools.js +131 -0
  24. package/dist/commands/templates/developer-tools.js.map +1 -0
  25. package/dist/commands/templates/ecommerce-platform.d.ts +5 -0
  26. package/dist/commands/templates/ecommerce-platform.js +144 -0
  27. package/dist/commands/templates/ecommerce-platform.js.map +1 -0
  28. package/dist/commands/templates/index.js +437 -23
  29. package/dist/commands/templates/index.js.map +1 -1
  30. package/dist/commands/templates/starter.d.ts +1 -0
  31. package/dist/commands/templates/starter.js +13 -8
  32. package/dist/commands/templates/starter.js.map +1 -1
  33. package/dist/commands/templates/usage-based.js +18 -4
  34. package/dist/commands/templates/usage-based.js.map +1 -1
  35. package/dist/utils/auth.d.ts +0 -5
  36. package/dist/utils/auth.js.map +1 -1
  37. package/dist/utils/config-loader.d.ts +0 -6
  38. package/dist/utils/config-loader.js +4 -0
  39. package/dist/utils/config-loader.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/commands/init.ts +6 -8
  42. package/src/commands/login.ts +0 -6
  43. package/src/commands/logout.ts +0 -5
  44. package/src/commands/pull.ts +148 -60
  45. package/src/commands/push.ts +67 -16
  46. package/src/commands/templates/ai-agent-platform.ts +114 -0
  47. package/src/commands/templates/b2b-saas.ts +23 -10
  48. package/src/commands/templates/developer-tools.ts +126 -0
  49. package/src/commands/templates/ecommerce-platform.ts +139 -0
  50. package/src/commands/templates/index.ts +6 -0
  51. package/src/commands/templates/starter.ts +14 -8
  52. package/src/commands/templates/usage-based.ts +18 -4
  53. package/src/utils/auth.ts +0 -6
  54. package/src/utils/config-loader.ts +6 -7
  55. package/tests/integration/pull.test.ts +6 -0
  56. package/tests/integration/push.test.ts +3 -0
@@ -2,6 +2,7 @@ interface TemplateConfig {
2
2
  features: string;
3
3
  addons: string;
4
4
  plans: string;
5
+ coupons: string;
5
6
  index: string;
6
7
  root: string;
7
8
  }
@@ -17,9 +17,9 @@ export const addons = {
17
17
  name: "5 Extra Seats",
18
18
  description: "Add 5 more team members to your workspace.",
19
19
  type: "recurring",
20
- prices: [
21
- { amount: 1500, currency: "USD", billing_interval: "monthly" }
22
- ],
20
+ amount: 1500,
21
+ currency: "USD",
22
+ billing_interval: "monthly",
23
23
  features: {
24
24
  seats: { value_limit: 5, type: "increment", is_hard_limit: false },
25
25
  }
@@ -28,9 +28,9 @@ export const addons = {
28
28
  name: "Priority Support",
29
29
  description: "24/7 Slack channel support.",
30
30
  type: "recurring",
31
- prices: [
32
- { amount: 9900, currency: "USD", billing_interval: "monthly" }
33
- ],
31
+ amount: 9900,
32
+ currency: "USD",
33
+ billing_interval: "monthly",
34
34
  features: {
35
35
  priority_support: { has_access: true },
36
36
  }
@@ -56,25 +56,30 @@ export const plans = {
56
56
  is_default: false,
57
57
  is_public: true,
58
58
  type: "paid",
59
- available_addons: ["extra_seats", "vip_support"],
60
59
  prices: [
61
- { amount: 2900, currency: "USD", billing_interval: "monthly", trial_period_days: 14 }
60
+ { amount: 2900, currency: "USD", billing_interval: "monthly", trial_period_days: 14, available_addons: ["extra_seats", "vip_support"] }
62
61
  ],
63
62
  features: {
64
63
  seats: { value_limit: 5, is_hard_limit: true },
65
64
  },
66
65
  }),
67
66
  };
67
+ `,
68
+ coupons: `import type { DiscountDef } from "@revstackhq/core";
69
+
70
+ export const coupons: DiscountDef[] = [];
68
71
  `,
69
72
  index: `import { defineConfig } from "@revstackhq/core";
70
73
  import { features } from "./features";
71
74
  import { addons } from "./addons";
72
75
  import { plans } from "./plans";
76
+ import { coupons } from "./coupons";
73
77
 
74
78
  export default defineConfig({
75
79
  features,
76
80
  addons,
77
81
  plans,
82
+ coupons,
78
83
  });
79
84
  `,
80
85
  root: `import config from "./revstack";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/templates/starter.ts"],"sourcesContent":["export interface TemplateConfig {\r\n features: string;\r\n addons: string;\r\n plans: string;\r\n index: string;\r\n root: string;\r\n}\r\n\r\nexport const starter: TemplateConfig = {\r\n features: `import { defineFeature } from \"@revstackhq/core\";\r\n\r\nexport const features = {\r\n seats: defineFeature({ name: \"Seats\", type: \"static\", unit_type: \"count\" }),\r\n priority_support: defineFeature({ name: \"Priority Support\", type: \"boolean\", unit_type: \"custom\" }),\r\n};\r\n`,\r\n addons: `import { defineAddon } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const addons = {\r\n extra_seats: defineAddon<typeof features>({\r\n name: \"5 Extra Seats\",\r\n description: \"Add 5 more team members to your workspace.\",\r\n type: \"recurring\",\r\n prices: [\r\n { amount: 1500, currency: \"USD\", billing_interval: \"monthly\" }\r\n ],\r\n features: {\r\n seats: { value_limit: 5, type: \"increment\", is_hard_limit: false },\r\n }\r\n }),\r\n vip_support: defineAddon<typeof features>({\r\n name: \"Priority Support\",\r\n description: \"24/7 Slack channel support.\",\r\n type: \"recurring\",\r\n prices: [\r\n { amount: 9900, currency: \"USD\", billing_interval: \"monthly\" }\r\n ],\r\n features: {\r\n priority_support: { has_access: true },\r\n }\r\n })\r\n};\r\n`,\r\n plans: `import { definePlan } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const plans = {\r\n // DO NOT DELETE: Automatically created default plan for guests.\r\n default: definePlan<typeof features>({\r\n name: \"Default\",\r\n description: \"Automatically created default plan for guests.\",\r\n is_default: true,\r\n is_public: false,\r\n type: \"free\",\r\n features: {},\r\n }),\r\n pro: definePlan<typeof features>({\r\n name: \"Pro\",\r\n description: \"For professional teams.\",\r\n is_default: false,\r\n is_public: true,\r\n type: \"paid\",\r\n available_addons: [\"extra_seats\", \"vip_support\"],\r\n prices: [\r\n { amount: 2900, currency: \"USD\", billing_interval: \"monthly\", trial_period_days: 14 }\r\n ],\r\n features: {\r\n seats: { value_limit: 5, is_hard_limit: true },\r\n },\r\n }),\r\n};\r\n`,\r\n index: `import { defineConfig } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\nimport { addons } from \"./addons\";\r\nimport { plans } from \"./plans\";\r\n\r\nexport default defineConfig({\r\n features,\r\n addons,\r\n plans,\r\n});\r\n`,\r\n root: `import config from \"./revstack\";\r\n\r\nexport default config;\r\n`,\r\n};\r\n"],"mappings":";;;AAQO,IAAM,UAA0B;AAAA,EACrC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BR,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWP,MAAM;AAAA;AAAA;AAAA;AAIR;","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/templates/starter.ts"],"sourcesContent":["export interface TemplateConfig {\r\n features: string;\r\n addons: string;\r\n plans: string;\r\n coupons: string;\r\n index: string;\r\n root: string;\r\n}\r\n\r\nexport const starter: TemplateConfig = {\r\n features: `import { defineFeature } from \"@revstackhq/core\";\r\n\r\nexport const features = {\r\n seats: defineFeature({ name: \"Seats\", type: \"static\", unit_type: \"count\" }),\r\n priority_support: defineFeature({ name: \"Priority Support\", type: \"boolean\", unit_type: \"custom\" }),\r\n};\r\n`,\r\n addons: `import { defineAddon } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const addons = {\r\n extra_seats: defineAddon<typeof features>({\r\n name: \"5 Extra Seats\",\r\n description: \"Add 5 more team members to your workspace.\",\r\n type: \"recurring\",\r\n amount: 1500,\r\n currency: \"USD\",\r\n billing_interval: \"monthly\",\r\n features: {\r\n seats: { value_limit: 5, type: \"increment\", is_hard_limit: false },\r\n }\r\n }),\r\n vip_support: defineAddon<typeof features>({\r\n name: \"Priority Support\",\r\n description: \"24/7 Slack channel support.\",\r\n type: \"recurring\",\r\n amount: 9900,\r\n currency: \"USD\",\r\n billing_interval: \"monthly\",\r\n features: {\r\n priority_support: { has_access: true },\r\n }\r\n })\r\n};\r\n`,\r\n plans: `import { definePlan } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const plans = {\r\n // DO NOT DELETE: Automatically created default plan for guests.\r\n default: definePlan<typeof features>({\r\n name: \"Default\",\r\n description: \"Automatically created default plan for guests.\",\r\n is_default: true,\r\n is_public: false,\r\n type: \"free\",\r\n features: {},\r\n }),\r\n pro: definePlan<typeof features>({\r\n name: \"Pro\",\r\n description: \"For professional teams.\",\r\n is_default: false,\r\n is_public: true,\r\n type: \"paid\",\r\n prices: [\r\n { amount: 2900, currency: \"USD\", billing_interval: \"monthly\", trial_period_days: 14, available_addons: [\"extra_seats\", \"vip_support\"] }\r\n ],\r\n features: {\r\n seats: { value_limit: 5, is_hard_limit: true },\r\n },\r\n }),\r\n};\r\n`,\r\n coupons: `import type { DiscountDef } from \"@revstackhq/core\";\r\n\r\nexport const coupons: DiscountDef[] = [];\r\n`,\r\n index: `import { defineConfig } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\nimport { addons } from \"./addons\";\r\nimport { plans } from \"./plans\";\r\nimport { coupons } from \"./coupons\";\r\n\r\nexport default defineConfig({\r\n features,\r\n addons,\r\n plans,\r\n coupons,\r\n});\r\n`,\r\n root: `import config from \"./revstack\";\r\n\r\nexport default config;\r\n`,\r\n};\r\n"],"mappings":";;;AASO,IAAM,UAA0B;AAAA,EACrC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BR,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BP,SAAS;AAAA;AAAA;AAAA;AAAA,EAIT,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaP,MAAM;AAAA;AAAA;AAAA;AAIR;","names":[]}
@@ -17,12 +17,24 @@ export const addons = {
17
17
  name: "Premium Support",
18
18
  description: "24/7 dedicated support.",
19
19
  type: "recurring",
20
- prices: [
21
- { amount: 20000, currency: "USD", billing_interval: "monthly" }
22
- ],
20
+ amount: 20000,
21
+ currency: "USD",
22
+ billing_interval: "monthly",
23
23
  features: {}
24
24
  })
25
25
  };
26
+ `,
27
+ coupons: `import type { DiscountDef } from "@revstackhq/core";
28
+
29
+ export const coupons: DiscountDef[] = [
30
+ {
31
+ code: "STARTUP_ACCELERATOR",
32
+ name: "Startup Program Credits",
33
+ type: "amount",
34
+ value: 500000, // $5,000 credit
35
+ duration: "forever",
36
+ }
37
+ ];
26
38
  `,
27
39
  plans: `import { definePlan } from "@revstackhq/core";
28
40
  import { features } from "./features";
@@ -42,12 +54,12 @@ export const plans = {
42
54
  is_default: false,
43
55
  is_public: true,
44
56
  type: "paid",
45
- available_addons: ["premium_support"],
46
57
  prices: [
47
58
  {
48
59
  amount: 0,
49
60
  currency: "USD",
50
61
  billing_interval: "monthly",
62
+ available_addons: ["premium_support"],
51
63
  overage_configuration: {
52
64
  api_requests: { overage_amount: 15, overage_unit: 1000 }, // $0.15 per 1k extra requests
53
65
  storage_gb: { overage_amount: 50, overage_unit: 1 }, // $0.50 per extra GB
@@ -65,11 +77,13 @@ export const plans = {
65
77
  import { features } from "./features";
66
78
  import { addons } from "./addons";
67
79
  import { plans } from "./plans";
80
+ import { coupons } from "./coupons";
68
81
 
69
82
  export default defineConfig({
70
83
  features,
71
84
  addons,
72
85
  plans,
86
+ coupons,
73
87
  });
74
88
  `,
75
89
  root: `import config from "./revstack";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/templates/usage-based.ts"],"sourcesContent":["import { TemplateConfig } from \"./starter\";\r\n\r\nexport const usageBased: TemplateConfig = {\r\n features: `import { defineFeature } from \"@revstackhq/core\";\r\n\r\nexport const features = {\r\n api_requests: defineFeature({ name: \"API Requests\", type: \"metered\", unit_type: \"requests\" }),\r\n storage_gb: defineFeature({ name: \"Storage (GB)\", type: \"metered\", unit_type: \"custom\" }),\r\n};\r\n`,\r\n addons: `import { defineAddon } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const addons = {\r\n premium_support: defineAddon<typeof features>({\r\n name: \"Premium Support\",\r\n description: \"24/7 dedicated support.\",\r\n type: \"recurring\",\r\n prices: [\r\n { amount: 20000, currency: \"USD\", billing_interval: \"monthly\" }\r\n ],\r\n features: {}\r\n })\r\n};\r\n`,\r\n plans: `import { definePlan } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const plans = {\r\n default: definePlan<typeof features>({\r\n name: \"Default\",\r\n description: \"Automatically created default plan for guests.\",\r\n is_default: true,\r\n is_public: false,\r\n type: \"free\",\r\n features: {},\r\n }),\r\n pay_as_you_go: definePlan<typeof features>({\r\n name: \"Pay As You Go\",\r\n description: \"Flexible usage-based pricing.\",\r\n is_default: false,\r\n is_public: true,\r\n type: \"paid\",\r\n available_addons: [\"premium_support\"],\r\n prices: [\r\n {\r\n amount: 0,\r\n currency: \"USD\",\r\n billing_interval: \"monthly\",\r\n overage_configuration: {\r\n api_requests: { overage_amount: 15, overage_unit: 1000 }, // $0.15 per 1k extra requests\r\n storage_gb: { overage_amount: 50, overage_unit: 1 }, // $0.50 per extra GB\r\n }\r\n } // Base platform fee\r\n ],\r\n features: {\r\n api_requests: { value_limit: 10000, is_hard_limit: false, reset_period: \"monthly\" }, // 10k free requests per month\r\n storage_gb: { value_limit: 5, is_hard_limit: false, reset_period: \"never\" }, // 5GB free storage lifetime\r\n },\r\n }),\r\n};\r\n`,\r\n index: `import { defineConfig } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\nimport { addons } from \"./addons\";\r\nimport { plans } from \"./plans\";\r\n\r\nexport default defineConfig({\r\n features,\r\n addons,\r\n plans,\r\n});\r\n`,\r\n root: `import config from \"./revstack\";\r\n\r\nexport default config;\r\n`,\r\n};\r\n"],"mappings":";;;AAEO,IAAM,aAA6B;AAAA,EACxC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeR,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqCP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWP,MAAM;AAAA;AAAA;AAAA;AAIR;","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/templates/usage-based.ts"],"sourcesContent":["import { TemplateConfig } from \"./starter\";\r\n\r\nexport const usageBased: TemplateConfig = {\r\n features: `import { defineFeature } from \"@revstackhq/core\";\r\n\r\nexport const features = {\r\n api_requests: defineFeature({ name: \"API Requests\", type: \"metered\", unit_type: \"requests\" }),\r\n storage_gb: defineFeature({ name: \"Storage (GB)\", type: \"metered\", unit_type: \"custom\" }),\r\n};\r\n`,\r\n addons: `import { defineAddon } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const addons = {\r\n premium_support: defineAddon<typeof features>({\r\n name: \"Premium Support\",\r\n description: \"24/7 dedicated support.\",\r\n type: \"recurring\",\r\n amount: 20000,\r\n currency: \"USD\",\r\n billing_interval: \"monthly\",\r\n features: {}\r\n })\r\n};\r\n`,\r\n coupons: `import type { DiscountDef } from \"@revstackhq/core\";\r\n\r\nexport const coupons: DiscountDef[] = [\r\n {\r\n code: \"STARTUP_ACCELERATOR\",\r\n name: \"Startup Program Credits\",\r\n type: \"amount\",\r\n value: 500000, // $5,000 credit\r\n duration: \"forever\",\r\n }\r\n];\r\n`,\r\n plans: `import { definePlan } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\n\r\nexport const plans = {\r\n default: definePlan<typeof features>({\r\n name: \"Default\",\r\n description: \"Automatically created default plan for guests.\",\r\n is_default: true,\r\n is_public: false,\r\n type: \"free\",\r\n features: {},\r\n }),\r\n pay_as_you_go: definePlan<typeof features>({\r\n name: \"Pay As You Go\",\r\n description: \"Flexible usage-based pricing.\",\r\n is_default: false,\r\n is_public: true,\r\n type: \"paid\",\r\n prices: [\r\n {\r\n amount: 0,\r\n currency: \"USD\",\r\n billing_interval: \"monthly\",\r\n available_addons: [\"premium_support\"],\r\n overage_configuration: {\r\n api_requests: { overage_amount: 15, overage_unit: 1000 }, // $0.15 per 1k extra requests\r\n storage_gb: { overage_amount: 50, overage_unit: 1 }, // $0.50 per extra GB\r\n }\r\n } // Base platform fee\r\n ],\r\n features: {\r\n api_requests: { value_limit: 10000, is_hard_limit: false, reset_period: \"monthly\" }, // 10k free requests per month\r\n storage_gb: { value_limit: 5, is_hard_limit: false, reset_period: \"never\" }, // 5GB free storage lifetime\r\n },\r\n }),\r\n};\r\n`,\r\n index: `import { defineConfig } from \"@revstackhq/core\";\r\nimport { features } from \"./features\";\r\nimport { addons } from \"./addons\";\r\nimport { plans } from \"./plans\";\r\nimport { coupons } from \"./coupons\";\r\n\r\nexport default defineConfig({\r\n features,\r\n addons,\r\n plans,\r\n coupons,\r\n});\r\n`,\r\n root: `import config from \"./revstack\";\r\n\r\nexport default config;\r\n`,\r\n};\r\n"],"mappings":";;;AAEO,IAAM,aAA6B;AAAA,EACxC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeR,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYT,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqCP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaP,MAAM;AAAA;AAAA;AAAA;AAIR;","names":[]}
@@ -1,8 +1,3 @@
1
- /**
2
- * @file utils/auth.ts
3
- * @description Manages global Revstack credentials stored at ~/.revstack/credentials.json.
4
- * Provides simple get/set helpers for the API key used by all authenticated commands.
5
- */
6
1
  /**
7
2
  * Persist an API key to the global credentials file.
8
3
  * Creates `~/.revstack/` if it doesn't exist.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/auth.ts"],"sourcesContent":["/**\n * @file utils/auth.ts\n * @description Manages global Revstack credentials stored at ~/.revstack/credentials.json.\n * Provides simple get/set helpers for the API key used by all authenticated commands.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nconst CREDENTIALS_DIR = path.join(os.homedir(), \".revstack\");\nconst CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, \"credentials.json\");\n\ninterface Credentials {\n apiKey: string;\n}\n\n/**\n * Persist an API key to the global credentials file.\n * Creates `~/.revstack/` if it doesn't exist.\n */\nexport function setApiKey(key: string): void {\n if (!fs.existsSync(CREDENTIALS_DIR)) {\n fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });\n }\n\n const credentials: Credentials = { apiKey: key };\n fs.writeFileSync(\n CREDENTIALS_FILE,\n JSON.stringify(credentials, null, 2),\n \"utf-8\",\n );\n}\n\n/**\n * Read the stored API key, or return `null` if none is configured.\n */\nexport function getApiKey(): string | null {\n if (!fs.existsSync(CREDENTIALS_FILE)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(CREDENTIALS_FILE, \"utf-8\");\n const credentials: Credentials = JSON.parse(raw);\n return credentials.apiKey ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove stored credentials. Used by `revstack logout`.\n */\nexport function clearApiKey(): void {\n if (fs.existsSync(CREDENTIALS_FILE)) {\n fs.unlinkSync(CREDENTIALS_FILE);\n }\n}\n"],"mappings":";;;AAMA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAEf,IAAM,kBAAkB,KAAK,KAAK,GAAG,QAAQ,GAAG,WAAW;AAC3D,IAAM,mBAAmB,KAAK,KAAK,iBAAiB,kBAAkB;AAU/D,SAAS,UAAU,KAAmB;AAC3C,MAAI,CAAC,GAAG,WAAW,eAAe,GAAG;AACnC,OAAG,UAAU,iBAAiB,EAAE,WAAW,KAAK,CAAC;AAAA,EACnD;AAEA,QAAM,cAA2B,EAAE,QAAQ,IAAI;AAC/C,KAAG;AAAA,IACD;AAAA,IACA,KAAK,UAAU,aAAa,MAAM,CAAC;AAAA,IACnC;AAAA,EACF;AACF;AAKO,SAAS,YAA2B;AACzC,MAAI,CAAC,GAAG,WAAW,gBAAgB,GAAG;AACpC,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,GAAG,aAAa,kBAAkB,OAAO;AACrD,UAAM,cAA2B,KAAK,MAAM,GAAG;AAC/C,WAAO,YAAY,UAAU;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,cAAoB;AAClC,MAAI,GAAG,WAAW,gBAAgB,GAAG;AACnC,OAAG,WAAW,gBAAgB;AAAA,EAChC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/auth.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nconst CREDENTIALS_DIR = path.join(os.homedir(), \".revstack\");\nconst CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, \"credentials.json\");\n\ninterface Credentials {\n apiKey: string;\n}\n\n/**\n * Persist an API key to the global credentials file.\n * Creates `~/.revstack/` if it doesn't exist.\n */\nexport function setApiKey(key: string): void {\n if (!fs.existsSync(CREDENTIALS_DIR)) {\n fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });\n }\n\n const credentials: Credentials = { apiKey: key };\n fs.writeFileSync(\n CREDENTIALS_FILE,\n JSON.stringify(credentials, null, 2),\n \"utf-8\",\n );\n}\n\n/**\n * Read the stored API key, or return `null` if none is configured.\n */\nexport function getApiKey(): string | null {\n if (!fs.existsSync(CREDENTIALS_FILE)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(CREDENTIALS_FILE, \"utf-8\");\n const credentials: Credentials = JSON.parse(raw);\n return credentials.apiKey ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove stored credentials. Used by `revstack logout`.\n */\nexport function clearApiKey(): void {\n if (fs.existsSync(CREDENTIALS_FILE)) {\n fs.unlinkSync(CREDENTIALS_FILE);\n }\n}\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAEf,IAAM,kBAAkB,KAAK,KAAK,GAAG,QAAQ,GAAG,WAAW;AAC3D,IAAM,mBAAmB,KAAK,KAAK,iBAAiB,kBAAkB;AAU/D,SAAS,UAAU,KAAmB;AAC3C,MAAI,CAAC,GAAG,WAAW,eAAe,GAAG;AACnC,OAAG,UAAU,iBAAiB,EAAE,WAAW,KAAK,CAAC;AAAA,EACnD;AAEA,QAAM,cAA2B,EAAE,QAAQ,IAAI;AAC/C,KAAG;AAAA,IACD;AAAA,IACA,KAAK,UAAU,aAAa,MAAM,CAAC;AAAA,IACnC;AAAA,EACF;AACF;AAKO,SAAS,YAA2B;AACzC,MAAI,CAAC,GAAG,WAAW,gBAAgB,GAAG;AACpC,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,GAAG,aAAa,kBAAkB,OAAO;AACrD,UAAM,cAA2B,KAAK,MAAM,GAAG;AAC/C,WAAO,YAAY,UAAU;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,cAAoB;AAClC,MAAI,GAAG,WAAW,gBAAgB,GAAG;AACnC,OAAG,WAAW,gBAAgB;AAAA,EAChC;AACF;","names":[]}
@@ -1,9 +1,3 @@
1
- /**
2
- * @file utils/config-loader.ts
3
- * @description Loads and evaluates the user's `revstack.config.ts` at runtime
4
- * using jiti (just-in-time TypeScript compilation). Returns a sanitized,
5
- * JSON-safe representation of the config for network transmission.
6
- */
7
1
  /**
8
2
  * Load the `revstack.config.ts` from the given directory.
9
3
  *
@@ -19,6 +19,10 @@ async function loadLocalConfig(cwd) {
19
19
  "\n \u2716 Could not find revstack.config.ts in the current directory.\n"
20
20
  ) + chalk.dim(" Run ") + chalk.bold("revstack init") + chalk.dim(" to create one.\n")
21
21
  );
22
+ } else if (err.name === "SyntaxError" || error instanceof SyntaxError) {
23
+ console.error(
24
+ chalk.red("\n \u2716 Syntax Error in revstack.config.ts\n") + chalk.dim(" " + (err.message ?? String(error))) + "\n"
25
+ );
22
26
  } else {
23
27
  console.error(
24
28
  chalk.red("\n \u2716 Failed to parse revstack.config.ts\n") + chalk.dim(" " + (err.message ?? String(error))) + "\n"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/config-loader.ts"],"sourcesContent":["/**\n * @file utils/config-loader.ts\n * @description Loads and evaluates the user's `revstack.config.ts` at runtime\n * using jiti (just-in-time TypeScript compilation). Returns a sanitized,\n * JSON-safe representation of the config for network transmission.\n */\n\nimport { createJiti } from \"jiti\";\nimport path from \"node:path\";\nimport chalk from \"chalk\";\n\n/**\n * Load the `revstack.config.ts` from the given directory.\n *\n * @param cwd - The directory to search for `revstack.config.ts`.\n * @returns The parsed and sanitized configuration object.\n */\nexport async function loadLocalConfig(\n cwd: string,\n): Promise<Record<string, unknown>> {\n const configPath = path.resolve(cwd, \"revstack.config.ts\");\n\n try {\n const jiti = createJiti(cwd);\n const module = (await jiti.import(configPath)) as Record<string, unknown>;\n const config = (module.default ?? module) as Record<string, unknown>;\n\n // Sanitize: strip functions, class instances, and non-serializable data.\n // This ensures we only send plain JSON to the Revstack Cloud API.\n return JSON.parse(JSON.stringify(config));\n } catch (error: unknown) {\n const err = error as NodeJS.ErrnoException;\n\n if (\n err.code === \"ERR_MODULE_NOT_FOUND\" ||\n err.code === \"ENOENT\" ||\n err.code === \"MODULE_NOT_FOUND\"\n ) {\n console.error(\n chalk.red(\n \"\\n ✖ Could not find revstack.config.ts in the current directory.\\n\",\n ) +\n chalk.dim(\" Run \") +\n chalk.bold(\"revstack init\") +\n chalk.dim(\" to create one.\\n\"),\n );\n } else {\n console.error(\n chalk.red(\"\\n ✖ Failed to parse revstack.config.ts\\n\") +\n chalk.dim(\" \" + (err.message ?? String(error))) +\n \"\\n\",\n );\n }\n\n process.exit(1);\n }\n}\n"],"mappings":";;;AAOA,SAAS,kBAAkB;AAC3B,OAAO,UAAU;AACjB,OAAO,WAAW;AAQlB,eAAsB,gBACpB,KACkC;AAClC,QAAM,aAAa,KAAK,QAAQ,KAAK,oBAAoB;AAEzD,MAAI;AACF,UAAM,OAAO,WAAW,GAAG;AAC3B,UAAM,SAAU,MAAM,KAAK,OAAO,UAAU;AAC5C,UAAM,SAAU,OAAO,WAAW;AAIlC,WAAO,KAAK,MAAM,KAAK,UAAU,MAAM,CAAC;AAAA,EAC1C,SAAS,OAAgB;AACvB,UAAM,MAAM;AAEZ,QACE,IAAI,SAAS,0BACb,IAAI,SAAS,YACb,IAAI,SAAS,oBACb;AACA,cAAQ;AAAA,QACN,MAAM;AAAA,UACJ;AAAA,QACF,IACE,MAAM,IAAI,UAAU,IACpB,MAAM,KAAK,eAAe,IAC1B,MAAM,IAAI,mBAAmB;AAAA,MACjC;AAAA,IACF,OAAO;AACL,cAAQ;AAAA,QACN,MAAM,IAAI,iDAA4C,IACpD,MAAM,IAAI,UAAU,IAAI,WAAW,OAAO,KAAK,EAAE,IACjD;AAAA,MACJ;AAAA,IACF;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/config-loader.ts"],"sourcesContent":["import { createJiti } from \"jiti\";\nimport path from \"node:path\";\nimport chalk from \"chalk\";\n\n/**\n * Load the `revstack.config.ts` from the given directory.\n *\n * @param cwd - The directory to search for `revstack.config.ts`.\n * @returns The parsed and sanitized configuration object.\n */\nexport async function loadLocalConfig(\n cwd: string,\n): Promise<Record<string, unknown>> {\n const configPath = path.resolve(cwd, \"revstack.config.ts\");\n\n try {\n const jiti = createJiti(cwd);\n const module = (await jiti.import(configPath)) as Record<string, unknown>;\n const config = (module.default ?? module) as Record<string, unknown>;\n\n // Sanitize: strip functions, class instances, and non-serializable data.\n // This ensures we only send plain JSON to the Revstack Cloud API.\n return JSON.parse(JSON.stringify(config));\n } catch (error: unknown) {\n const err = error as NodeJS.ErrnoException;\n\n if (\n err.code === \"ERR_MODULE_NOT_FOUND\" ||\n err.code === \"ENOENT\" ||\n err.code === \"MODULE_NOT_FOUND\"\n ) {\n console.error(\n chalk.red(\n \"\\n ✖ Could not find revstack.config.ts in the current directory.\\n\",\n ) +\n chalk.dim(\" Run \") +\n chalk.bold(\"revstack init\") +\n chalk.dim(\" to create one.\\n\"),\n );\n } else if (err.name === \"SyntaxError\" || error instanceof SyntaxError) {\n console.error(\n chalk.red(\"\\n ✖ Syntax Error in revstack.config.ts\\n\") +\n chalk.dim(\" \" + (err.message ?? String(error))) +\n \"\\n\",\n );\n } else {\n console.error(\n chalk.red(\"\\n ✖ Failed to parse revstack.config.ts\\n\") +\n chalk.dim(\" \" + (err.message ?? String(error))) +\n \"\\n\",\n );\n }\n\n process.exit(1);\n }\n}\n"],"mappings":";;;AAAA,SAAS,kBAAkB;AAC3B,OAAO,UAAU;AACjB,OAAO,WAAW;AAQlB,eAAsB,gBACpB,KACkC;AAClC,QAAM,aAAa,KAAK,QAAQ,KAAK,oBAAoB;AAEzD,MAAI;AACF,UAAM,OAAO,WAAW,GAAG;AAC3B,UAAM,SAAU,MAAM,KAAK,OAAO,UAAU;AAC5C,UAAM,SAAU,OAAO,WAAW;AAIlC,WAAO,KAAK,MAAM,KAAK,UAAU,MAAM,CAAC;AAAA,EAC1C,SAAS,OAAgB;AACvB,UAAM,MAAM;AAEZ,QACE,IAAI,SAAS,0BACb,IAAI,SAAS,YACb,IAAI,SAAS,oBACb;AACA,cAAQ;AAAA,QACN,MAAM;AAAA,UACJ;AAAA,QACF,IACE,MAAM,IAAI,UAAU,IACpB,MAAM,KAAK,eAAe,IAC1B,MAAM,IAAI,mBAAmB;AAAA,MACjC;AAAA,IACF,WAAW,IAAI,SAAS,iBAAiB,iBAAiB,aAAa;AACrE,cAAQ;AAAA,QACN,MAAM,IAAI,iDAA4C,IACpD,MAAM,IAAI,UAAU,IAAI,WAAW,OAAO,KAAK,EAAE,IACjD;AAAA,MACJ;AAAA,IACF,OAAO;AACL,cAAQ;AAAA,QACN,MAAM,IAAI,iDAA4C,IACpD,MAAM,IAAI,UAAU,IAAI,WAAW,OAAO,KAAK,EAAE,IACjD;AAAA,MACJ;AAAA,IACF;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revstackhq/cli",
3
- "version": "0.0.0-dev-20260227103607",
3
+ "version": "0.0.0-dev-20260228060138",
4
4
  "description": "The official CLI for Revstack — Billing as Code",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "jiti": "^2.4.2",
16
16
  "ora": "^8.2.0",
17
17
  "prompts": "^2.4.2",
18
- "@revstackhq/core": "0.0.0-dev-20260227103607"
18
+ "@revstackhq/core": "0.0.0-dev-20260228060138"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.11.0",
@@ -1,10 +1,3 @@
1
- /**
2
- * @file commands/init.ts
3
- * @description Scaffolds a new `revstack.config.ts` in the current directory.
4
- * Generates a starter config with the immutable Default Guest Plan and
5
- * a sample Pro plan using type-safe helpers from @revstackhq/core.
6
- */
7
-
8
1
  import { Command } from "commander";
9
2
  import chalk from "chalk";
10
3
  import fs from "node:fs";
@@ -19,7 +12,7 @@ export const initCommand = new Command("init")
19
12
  .description("Scaffold a new revstack.config.ts in the current directory")
20
13
  .option(
21
14
  "-t, --template <name>",
22
- "Choose a starting template (starter, b2b-saas, usage-based)",
15
+ `Choose a starting template (${Object.keys(TEMPLATES).join(", ")})`,
23
16
  "starter",
24
17
  )
25
18
  .action(async (options) => {
@@ -62,6 +55,11 @@ export const initCommand = new Command("init")
62
55
  template.addons,
63
56
  "utf-8",
64
57
  );
58
+ fs.writeFileSync(
59
+ path.resolve(revstackDir, "coupons.ts"),
60
+ template.coupons,
61
+ "utf-8",
62
+ );
65
63
  fs.writeFileSync(
66
64
  path.resolve(revstackDir, "plans.ts"),
67
65
  template.plans,
@@ -1,9 +1,3 @@
1
- /**
2
- * @file commands/login.ts
3
- * @description Interactive authentication flow. Prompts the user for their
4
- * Revstack Secret Key and stores it globally for subsequent CLI commands.
5
- */
6
-
7
1
  import { Command } from "commander";
8
2
  import chalk from "chalk";
9
3
  import prompts from "prompts";
@@ -1,8 +1,3 @@
1
- /**
2
- * @file commands/logout.ts
3
- * @description Clears stored Revstack credentials.
4
- */
5
-
6
1
  import { Command } from "commander";
7
2
  import chalk from "chalk";
8
3
  import { clearApiKey, getApiKey } from "@/utils/auth.js";
@@ -1,56 +1,12 @@
1
- /**
2
- * @file commands/pull.ts
3
- * @description Fetches the current billing configuration from Revstack Cloud
4
- * and writes it back to the local `revstack.config.ts` file, overwriting
5
- * the existing config after user confirmation.
6
- */
7
-
8
1
  import { Command } from "commander";
9
2
  import chalk from "chalk";
10
3
  import ora from "ora";
11
4
  import prompts from "prompts";
12
5
  import fs from "node:fs";
13
6
  import path from "node:path";
7
+ import { execa } from "execa";
14
8
  import { getApiKey } from "@/utils/auth";
15
-
16
- // ─── Types ───────────────────────────────────────────────────
17
-
18
- interface RemoteFeature {
19
- name: string;
20
- type: string;
21
- unit_type: string;
22
- description?: string;
23
- }
24
-
25
- interface RemotePrice {
26
- amount: number;
27
- currency: string;
28
- billing_interval: string;
29
- trial_period_days?: number;
30
- }
31
-
32
- interface RemotePlanFeature {
33
- value_limit?: number;
34
- value_bool?: boolean;
35
- value_text?: string;
36
- is_hard_limit?: boolean;
37
- reset_period?: string;
38
- }
39
-
40
- interface RemotePlan {
41
- name: string;
42
- description?: string;
43
- is_default: boolean;
44
- is_public: boolean;
45
- type: string;
46
- prices?: RemotePrice[];
47
- features: Record<string, RemotePlanFeature>;
48
- }
49
-
50
- interface RemoteConfig {
51
- features: Record<string, RemoteFeature>;
52
- plans: Record<string, RemotePlan>;
53
- }
9
+ import { type RevstackConfig, RevstackConfigSchema } from "@revstackhq/core";
54
10
 
55
11
  function serializeObject(
56
12
  obj: Record<string, unknown>,
@@ -100,7 +56,7 @@ function serializeArray(arr: unknown[], depth: number): string {
100
56
  return `[\n${items.join("\n")}\n${closePad}]`;
101
57
  }
102
58
 
103
- function generateFeaturesSource(config: RemoteConfig): string {
59
+ function generateFeaturesSource(config: RevstackConfig): string {
104
60
  const featureEntries = Object.entries(config.features).map(([slug, f]) => {
105
61
  const props: Record<string, unknown> = {
106
62
  name: f.name,
@@ -120,7 +76,7 @@ ${featureEntries.join("\n")}
120
76
  `;
121
77
  }
122
78
 
123
- function generatePlansSource(config: RemoteConfig): string {
79
+ function generatePlansSource(config: RevstackConfig): string {
124
80
  const planEntries = Object.entries(config.plans).map(([slug, plan]) => {
125
81
  const props: Record<string, unknown> = {
126
82
  name: plan.name,
@@ -152,15 +108,99 @@ ${planEntries.join("\n")}
152
108
  `;
153
109
  }
154
110
 
155
- function generateRootConfigSource(): string {
156
- return `import { defineConfig } from "@revstackhq/core";
157
- import { features } from "./revstack/features";
158
- import { plans } from "./revstack/plans";
111
+ function generateAddonsSource(config: RevstackConfig): string {
112
+ if (!config.addons) return "";
159
113
 
160
- export default defineConfig({
161
- features,
162
- plans,
163
- });
114
+ const addonEntries = Object.entries(config.addons).map(([slug, addon]) => {
115
+ const props: Record<string, unknown> = {
116
+ name: addon.name,
117
+ type: addon.type,
118
+ amount: addon.amount,
119
+ currency: addon.currency,
120
+ };
121
+ if (addon.description) props.description = addon.description;
122
+ if (addon.billing_interval) props.billing_interval = addon.billing_interval;
123
+ props.features = addon.features;
124
+
125
+ return ` ${slug}: defineAddon<typeof features>(${serializeObject(props, 2)}),`;
126
+ });
127
+
128
+ return `import { defineAddon } from "@revstackhq/core";
129
+ import { features } from "./features";
130
+
131
+ export const addons = {
132
+ ${addonEntries.join("\n")}
133
+ };
134
+ `;
135
+ }
136
+
137
+ function generateCouponsSource(config: RevstackConfig): string {
138
+ if (!config.coupons || config.coupons.length === 0) return "";
139
+
140
+ // Make sure we format date strings and numbers properly but let serializeArray sort deep nesting
141
+ const couponsArray = config.coupons.map((coupon) => {
142
+ const props: Record<string, unknown> = {
143
+ code: coupon.code,
144
+ type: coupon.type,
145
+ value: coupon.value,
146
+ duration: coupon.duration,
147
+ };
148
+ if (coupon.name) props.name = coupon.name;
149
+ if (coupon.duration_in_months)
150
+ props.duration_in_months = coupon.duration_in_months;
151
+ if (coupon.applies_to_plans)
152
+ props.applies_to_plans = coupon.applies_to_plans;
153
+ if (coupon.max_redemptions) props.max_redemptions = coupon.max_redemptions;
154
+ if (coupon.expires_at) props.expires_at = coupon.expires_at;
155
+
156
+ return props;
157
+ });
158
+
159
+ return `import type { DiscountDef } from "@revstackhq/core";
160
+
161
+ export const coupons: DiscountDef[] = ${serializeArray(couponsArray, 0)};
162
+ `;
163
+ }
164
+
165
+ function generateRootConfigSource(config: RevstackConfig): string {
166
+ const hasAddons = !!config.addons && Object.keys(config.addons).length > 0;
167
+ const hasCoupons = !!config.coupons && config.coupons.length > 0;
168
+
169
+ const imports = [
170
+ `import { defineConfig } from "@revstackhq/core";`,
171
+ `import { features } from "./revstack/features";`,
172
+ `import { plans } from "./revstack/plans";`,
173
+ ];
174
+ if (hasAddons) {
175
+ imports.push(`import { addons } from "./revstack/addons";`);
176
+ }
177
+ if (hasCoupons) {
178
+ imports.push(`import { coupons } from "./revstack/coupons";`);
179
+ }
180
+
181
+ const configProps: Record<string, unknown> = {
182
+ features: "__RAW_features",
183
+ plans: "__RAW_plans",
184
+ };
185
+
186
+ if (hasAddons) {
187
+ configProps.addons = "__RAW_addons";
188
+ }
189
+
190
+ if (hasCoupons) {
191
+ configProps.coupons = "__RAW_coupons";
192
+ }
193
+
194
+ // Serialize the config object, but then remove the quotes around our RAW variables
195
+ let configStr = serializeObject(configProps, 0);
196
+ configStr = configStr.replace(/"__RAW_features"/g, "features");
197
+ configStr = configStr.replace(/"__RAW_plans"/g, "plans");
198
+ configStr = configStr.replace(/"__RAW_addons"/g, "addons");
199
+ configStr = configStr.replace(/"__RAW_coupons"/g, "coupons");
200
+
201
+ return `${imports.join("\n")}
202
+
203
+ export default defineConfig(${configStr});
164
204
  `;
165
205
  }
166
206
 
@@ -195,7 +235,7 @@ export const pullCommand = new Command("pull")
195
235
  prefixText: " ",
196
236
  }).start();
197
237
 
198
- let remoteConfig: RemoteConfig;
238
+ let remoteConfig: RevstackConfig;
199
239
 
200
240
  try {
201
241
  const res = await fetch(
@@ -213,7 +253,16 @@ export const pullCommand = new Command("pull")
213
253
  process.exit(1);
214
254
  }
215
255
 
216
- remoteConfig = (await res.json()) as RemoteConfig;
256
+ const rawData = await res.json();
257
+
258
+ try {
259
+ remoteConfig = RevstackConfigSchema.parse(rawData);
260
+ } catch (validationError: any) {
261
+ spinner.fail("Remote config failed schema validation");
262
+ console.error(chalk.red(`\n ${validationError.message}\n`));
263
+ process.exit(1);
264
+ }
265
+
217
266
  spinner.succeed("Remote config fetched");
218
267
  } catch (error: unknown) {
219
268
  spinner.fail("Failed to reach Revstack Cloud");
@@ -224,11 +273,17 @@ export const pullCommand = new Command("pull")
224
273
  // ── 2. Show summary ────────────────────────────────────
225
274
  const featureCount = Object.keys(remoteConfig.features).length;
226
275
  const planCount = Object.keys(remoteConfig.plans).length;
276
+ const addonCount = remoteConfig.addons
277
+ ? Object.keys(remoteConfig.addons).length
278
+ : 0;
279
+ const couponCount = remoteConfig.coupons ? remoteConfig.coupons.length : 0;
227
280
 
228
281
  console.log(
229
282
  "\n" +
230
283
  chalk.dim(" Remote state: ") +
231
- chalk.white(`${featureCount} features, ${planCount} plans`) +
284
+ chalk.white(
285
+ `${featureCount} features, ${planCount} plans, ${addonCount} addons, ${couponCount} coupons`,
286
+ ) +
232
287
  chalk.dim(` (${options.env})\n`),
233
288
  );
234
289
 
@@ -264,12 +319,45 @@ export const pullCommand = new Command("pull")
264
319
 
265
320
  const featuresSource = generateFeaturesSource(remoteConfig);
266
321
  const plansSource = generatePlansSource(remoteConfig);
267
- const rootSource = generateRootConfigSource();
322
+ const rootSource = generateRootConfigSource(remoteConfig);
268
323
 
269
324
  fs.writeFileSync(featuresPath, featuresSource, "utf-8");
270
325
  fs.writeFileSync(plansPath, plansSource, "utf-8");
271
326
  fs.writeFileSync(configPath, rootSource, "utf-8");
272
327
 
328
+ if (remoteConfig.addons && Object.keys(remoteConfig.addons).length > 0) {
329
+ const addonsSource = generateAddonsSource(remoteConfig);
330
+ const addonsPath = path.resolve(revstackDir, "addons.ts");
331
+ fs.writeFileSync(addonsPath, addonsSource, "utf-8");
332
+ }
333
+
334
+ if (remoteConfig.coupons && remoteConfig.coupons.length > 0) {
335
+ const couponsSource = generateCouponsSource(remoteConfig);
336
+ const couponsPath = path.resolve(revstackDir, "coupons.ts");
337
+ fs.writeFileSync(couponsPath, couponsSource, "utf-8");
338
+ }
339
+
340
+ // ── 5. Format Files ────────────────────────────────────
341
+ const formatSpinner = ora("Formatting generated files...").start();
342
+ try {
343
+ // Run prettier on the generated files. We use npx to ensure it runs
344
+ // using the locally hoisted version of prettier.
345
+ await execa(
346
+ "npx",
347
+ ["prettier", "--write", "revstack.config.ts", "revstack/**/*.ts"],
348
+ {
349
+ cwd,
350
+ stdio: "ignore",
351
+ },
352
+ );
353
+ formatSpinner.succeed("Files formatted successfully");
354
+ } catch (err) {
355
+ // It's not the end of the world if prettier fails, just notify
356
+ formatSpinner.info(
357
+ "Files written properly, but automatic formatting (prettier) skipped or failed.",
358
+ );
359
+ }
360
+
273
361
  console.log(
274
362
  "\n" +
275
363
  chalk.green(" ✔ Local files updated from remote.\n") +