@revstackhq/cli 0.0.0-dev-20260226063200 → 0.0.0-dev-20260227092523

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 (42) hide show
  1. package/.turbo/turbo-build.log +42 -30
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +58 -38
  4. package/dist/cli.js +314 -43
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/init.js +279 -42
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/login.js.map +1 -1
  9. package/dist/commands/logout.js +3 -1
  10. package/dist/commands/logout.js.map +1 -1
  11. package/dist/commands/pull.js.map +1 -1
  12. package/dist/commands/push.js +32 -0
  13. package/dist/commands/push.js.map +1 -1
  14. package/dist/commands/templates/b2b-saas.d.ts +5 -0
  15. package/dist/commands/templates/b2b-saas.js +104 -0
  16. package/dist/commands/templates/b2b-saas.js.map +1 -0
  17. package/dist/commands/templates/index.d.ts +5 -0
  18. package/dist/commands/templates/index.js +264 -0
  19. package/dist/commands/templates/index.js.map +1 -0
  20. package/dist/commands/templates/starter.d.ts +10 -0
  21. package/dist/commands/templates/starter.js +88 -0
  22. package/dist/commands/templates/starter.js.map +1 -0
  23. package/dist/commands/templates/usage-based.d.ts +5 -0
  24. package/dist/commands/templates/usage-based.js +75 -0
  25. package/dist/commands/templates/usage-based.js.map +1 -0
  26. package/dist/utils/auth.js.map +1 -1
  27. package/dist/utils/config-loader.js.map +1 -1
  28. package/package.json +3 -2
  29. package/src/cli.ts +32 -32
  30. package/src/commands/init.ts +187 -210
  31. package/src/commands/login.ts +39 -39
  32. package/src/commands/logout.ts +27 -25
  33. package/src/commands/pull.ts +280 -280
  34. package/src/commands/push.ts +244 -206
  35. package/src/commands/templates/b2b-saas.ts +99 -0
  36. package/src/commands/templates/index.ts +12 -0
  37. package/src/commands/templates/starter.ts +89 -0
  38. package/src/commands/templates/usage-based.ts +70 -0
  39. package/src/utils/auth.ts +59 -59
  40. package/src/utils/config-loader.ts +57 -57
  41. package/tests/integration/init.test.ts +12 -2
  42. package/tests/integration/push.test.ts +20 -4
@@ -1,280 +1,280 @@
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
- import { Command } from "commander";
9
- import chalk from "chalk";
10
- import ora from "ora";
11
- import prompts from "prompts";
12
- import fs from "node:fs";
13
- import path from "node:path";
14
- 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
- }
54
-
55
- function serializeObject(
56
- obj: Record<string, unknown>,
57
- depth: number = 0,
58
- ): string {
59
- const entries = Object.entries(obj);
60
- if (entries.length === 0) return "{}";
61
-
62
- const pad = " ".repeat(depth + 1);
63
- const closePad = " ".repeat(depth);
64
-
65
- const lines = entries
66
- .map(([key, value]) => {
67
- if (value === undefined) return null;
68
-
69
- const formattedValue =
70
- typeof value === "string"
71
- ? `"${value}"`
72
- : typeof value === "number" || typeof value === "boolean"
73
- ? String(value)
74
- : Array.isArray(value)
75
- ? serializeArray(value, depth + 1)
76
- : typeof value === "object" && value !== null
77
- ? serializeObject(value as Record<string, unknown>, depth + 1)
78
- : String(value);
79
-
80
- return `${pad}${key}: ${formattedValue},`;
81
- })
82
- .filter(Boolean);
83
-
84
- return `{\n${lines.join("\n")}\n${closePad}}`;
85
- }
86
-
87
- function serializeArray(arr: unknown[], depth: number): string {
88
- if (arr.length === 0) return "[]";
89
-
90
- const pad = " ".repeat(depth + 1);
91
- const closePad = " ".repeat(depth);
92
-
93
- const items = arr.map((item) => {
94
- if (typeof item === "object" && item !== null) {
95
- return `${pad}${serializeObject(item as Record<string, unknown>, depth + 1)},`;
96
- }
97
- return `${pad}${JSON.stringify(item)},`;
98
- });
99
-
100
- return `[\n${items.join("\n")}\n${closePad}]`;
101
- }
102
-
103
- function generateFeaturesSource(config: RemoteConfig): string {
104
- const featureEntries = Object.entries(config.features).map(([slug, f]) => {
105
- const props: Record<string, unknown> = {
106
- name: f.name,
107
- type: f.type,
108
- unit_type: f.unit_type,
109
- };
110
- if (f.description) props.description = f.description;
111
-
112
- return ` ${slug}: defineFeature(${serializeObject(props, 2)}),`;
113
- });
114
-
115
- return `import { defineFeature } from "@revstackhq/core";
116
-
117
- export const features = {
118
- ${featureEntries.join("\n")}
119
- };
120
- `;
121
- }
122
-
123
- function generatePlansSource(config: RemoteConfig): string {
124
- const planEntries = Object.entries(config.plans).map(([slug, plan]) => {
125
- const props: Record<string, unknown> = {
126
- name: plan.name,
127
- };
128
- if (plan.description) props.description = plan.description;
129
- props.is_default = plan.is_default;
130
- props.is_public = plan.is_public;
131
- props.type = plan.type;
132
-
133
- if (plan.prices && plan.prices.length > 0) {
134
- props.prices = plan.prices;
135
- }
136
-
137
- props.features = plan.features;
138
-
139
- const comment = plan.is_default
140
- ? ` // DO NOT DELETE: Automatically created default plan for guests.\n`
141
- : "";
142
-
143
- return `${comment} ${slug}: definePlan<typeof features>(${serializeObject(props, 3)}),`;
144
- });
145
-
146
- return `import { definePlan } from "@revstackhq/core";
147
- import { features } from "./features";
148
-
149
- export const plans = {
150
- ${planEntries.join("\n")}
151
- };
152
- `;
153
- }
154
-
155
- function generateRootConfigSource(): string {
156
- return `import { defineConfig } from "@revstackhq/core";
157
- import { features } from "./revstack/features";
158
- import { plans } from "./revstack/plans";
159
-
160
- export default defineConfig({
161
- features,
162
- plans,
163
- });
164
- `;
165
- }
166
-
167
- // ─── Helpers ─────────────────────────────────────────────────
168
-
169
- const API_BASE = "https://app.revstack.dev";
170
-
171
- // ─── Command ─────────────────────────────────────────────────
172
-
173
- export const pullCommand = new Command("pull")
174
- .description(
175
- "Pull the remote billing config and overwrite local revstack.config.ts and revstack/ files",
176
- )
177
- .option("-e, --env <environment>", "Target environment", "test")
178
- .action(async (options: { env: string }) => {
179
- const apiKey = getApiKey();
180
-
181
- if (!apiKey) {
182
- console.error(
183
- "\n" +
184
- chalk.red(" ✖ Not authenticated.\n") +
185
- chalk.dim(" Run ") +
186
- chalk.bold("revstack login") +
187
- chalk.dim(" first.\n"),
188
- );
189
- process.exit(1);
190
- }
191
-
192
- // ── 1. Fetch remote config ─────────────────────────────
193
- const spinner = ora({
194
- text: "Fetching remote configuration...",
195
- prefixText: " ",
196
- }).start();
197
-
198
- let remoteConfig: RemoteConfig;
199
-
200
- try {
201
- const res = await fetch(
202
- `${API_BASE}/api/v1/cli/pull?env=${encodeURIComponent(options.env)}`,
203
- {
204
- headers: { Authorization: `Bearer ${apiKey}` },
205
- },
206
- );
207
-
208
- if (!res.ok) {
209
- spinner.fail("Failed to fetch remote config");
210
- console.error(
211
- chalk.red(`\n API returned ${res.status}: ${res.statusText}\n`),
212
- );
213
- process.exit(1);
214
- }
215
-
216
- remoteConfig = (await res.json()) as RemoteConfig;
217
- spinner.succeed("Remote config fetched");
218
- } catch (error: unknown) {
219
- spinner.fail("Failed to reach Revstack Cloud");
220
- console.error(chalk.red(`\n ${(error as Error).message}\n`));
221
- process.exit(1);
222
- }
223
-
224
- // ── 2. Show summary ────────────────────────────────────
225
- const featureCount = Object.keys(remoteConfig.features).length;
226
- const planCount = Object.keys(remoteConfig.plans).length;
227
-
228
- console.log(
229
- "\n" +
230
- chalk.dim(" Remote state: ") +
231
- chalk.white(`${featureCount} features, ${planCount} plans`) +
232
- chalk.dim(` (${options.env})\n`),
233
- );
234
-
235
- // ── 3. Confirm overwrite ───────────────────────────────
236
- const cwd = process.cwd();
237
- const configPath = path.resolve(cwd, "revstack.config.ts");
238
- const revstackDir = path.resolve(cwd, "revstack");
239
- const featuresPath = path.resolve(revstackDir, "features.ts");
240
- const plansPath = path.resolve(revstackDir, "plans.ts");
241
-
242
- const rootExists = fs.existsSync(configPath);
243
- const dirExists = fs.existsSync(revstackDir);
244
-
245
- if (rootExists || dirExists) {
246
- const { confirm } = await prompts({
247
- type: "confirm",
248
- name: "confirm",
249
- message:
250
- "This will overwrite your local configuration files (revstack.config.ts and revstack/ data). Are you sure?",
251
- initial: false,
252
- });
253
-
254
- if (!confirm) {
255
- console.log(chalk.dim("\n Pull cancelled.\n"));
256
- return;
257
- }
258
- }
259
-
260
- // ── 4. Generate and write ──────────────────────────────
261
- if (!fs.existsSync(revstackDir)) {
262
- fs.mkdirSync(revstackDir, { recursive: true });
263
- }
264
-
265
- const featuresSource = generateFeaturesSource(remoteConfig);
266
- const plansSource = generatePlansSource(remoteConfig);
267
- const rootSource = generateRootConfigSource();
268
-
269
- fs.writeFileSync(featuresPath, featuresSource, "utf-8");
270
- fs.writeFileSync(plansPath, plansSource, "utf-8");
271
- fs.writeFileSync(configPath, rootSource, "utf-8");
272
-
273
- console.log(
274
- "\n" +
275
- chalk.green(" ✔ Local files updated from remote.\n") +
276
- chalk.dim(" Review the files and run ") +
277
- chalk.bold("revstack push") +
278
- chalk.dim(" to re-deploy.\n"),
279
- );
280
- });
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
+ import { Command } from "commander";
9
+ import chalk from "chalk";
10
+ import ora from "ora";
11
+ import prompts from "prompts";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ 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
+ }
54
+
55
+ function serializeObject(
56
+ obj: Record<string, unknown>,
57
+ depth: number = 0,
58
+ ): string {
59
+ const entries = Object.entries(obj);
60
+ if (entries.length === 0) return "{}";
61
+
62
+ const pad = " ".repeat(depth + 1);
63
+ const closePad = " ".repeat(depth);
64
+
65
+ const lines = entries
66
+ .map(([key, value]) => {
67
+ if (value === undefined) return null;
68
+
69
+ const formattedValue =
70
+ typeof value === "string"
71
+ ? `"${value}"`
72
+ : typeof value === "number" || typeof value === "boolean"
73
+ ? String(value)
74
+ : Array.isArray(value)
75
+ ? serializeArray(value, depth + 1)
76
+ : typeof value === "object" && value !== null
77
+ ? serializeObject(value as Record<string, unknown>, depth + 1)
78
+ : String(value);
79
+
80
+ return `${pad}${key}: ${formattedValue},`;
81
+ })
82
+ .filter(Boolean);
83
+
84
+ return `{\n${lines.join("\n")}\n${closePad}}`;
85
+ }
86
+
87
+ function serializeArray(arr: unknown[], depth: number): string {
88
+ if (arr.length === 0) return "[]";
89
+
90
+ const pad = " ".repeat(depth + 1);
91
+ const closePad = " ".repeat(depth);
92
+
93
+ const items = arr.map((item) => {
94
+ if (typeof item === "object" && item !== null) {
95
+ return `${pad}${serializeObject(item as Record<string, unknown>, depth + 1)},`;
96
+ }
97
+ return `${pad}${JSON.stringify(item)},`;
98
+ });
99
+
100
+ return `[\n${items.join("\n")}\n${closePad}]`;
101
+ }
102
+
103
+ function generateFeaturesSource(config: RemoteConfig): string {
104
+ const featureEntries = Object.entries(config.features).map(([slug, f]) => {
105
+ const props: Record<string, unknown> = {
106
+ name: f.name,
107
+ type: f.type,
108
+ unit_type: f.unit_type,
109
+ };
110
+ if (f.description) props.description = f.description;
111
+
112
+ return ` ${slug}: defineFeature(${serializeObject(props, 2)}),`;
113
+ });
114
+
115
+ return `import { defineFeature } from "@revstackhq/core";
116
+
117
+ export const features = {
118
+ ${featureEntries.join("\n")}
119
+ };
120
+ `;
121
+ }
122
+
123
+ function generatePlansSource(config: RemoteConfig): string {
124
+ const planEntries = Object.entries(config.plans).map(([slug, plan]) => {
125
+ const props: Record<string, unknown> = {
126
+ name: plan.name,
127
+ };
128
+ if (plan.description) props.description = plan.description;
129
+ props.is_default = plan.is_default;
130
+ props.is_public = plan.is_public;
131
+ props.type = plan.type;
132
+
133
+ if (plan.prices && plan.prices.length > 0) {
134
+ props.prices = plan.prices;
135
+ }
136
+
137
+ props.features = plan.features;
138
+
139
+ const comment = plan.is_default
140
+ ? ` // DO NOT DELETE: Automatically created default plan for guests.\n`
141
+ : "";
142
+
143
+ return `${comment} ${slug}: definePlan<typeof features>(${serializeObject(props, 3)}),`;
144
+ });
145
+
146
+ return `import { definePlan } from "@revstackhq/core";
147
+ import { features } from "./features";
148
+
149
+ export const plans = {
150
+ ${planEntries.join("\n")}
151
+ };
152
+ `;
153
+ }
154
+
155
+ function generateRootConfigSource(): string {
156
+ return `import { defineConfig } from "@revstackhq/core";
157
+ import { features } from "./revstack/features";
158
+ import { plans } from "./revstack/plans";
159
+
160
+ export default defineConfig({
161
+ features,
162
+ plans,
163
+ });
164
+ `;
165
+ }
166
+
167
+ // ─── Helpers ─────────────────────────────────────────────────
168
+
169
+ const API_BASE = "https://app.revstack.dev";
170
+
171
+ // ─── Command ─────────────────────────────────────────────────
172
+
173
+ export const pullCommand = new Command("pull")
174
+ .description(
175
+ "Pull the remote billing config and overwrite local revstack.config.ts and revstack/ files",
176
+ )
177
+ .option("-e, --env <environment>", "Target environment", "test")
178
+ .action(async (options: { env: string }) => {
179
+ const apiKey = getApiKey();
180
+
181
+ if (!apiKey) {
182
+ console.error(
183
+ "\n" +
184
+ chalk.red(" ✖ Not authenticated.\n") +
185
+ chalk.dim(" Run ") +
186
+ chalk.bold("revstack login") +
187
+ chalk.dim(" first.\n"),
188
+ );
189
+ process.exit(1);
190
+ }
191
+
192
+ // ── 1. Fetch remote config ─────────────────────────────
193
+ const spinner = ora({
194
+ text: "Fetching remote configuration...",
195
+ prefixText: " ",
196
+ }).start();
197
+
198
+ let remoteConfig: RemoteConfig;
199
+
200
+ try {
201
+ const res = await fetch(
202
+ `${API_BASE}/api/v1/cli/pull?env=${encodeURIComponent(options.env)}`,
203
+ {
204
+ headers: { Authorization: `Bearer ${apiKey}` },
205
+ },
206
+ );
207
+
208
+ if (!res.ok) {
209
+ spinner.fail("Failed to fetch remote config");
210
+ console.error(
211
+ chalk.red(`\n API returned ${res.status}: ${res.statusText}\n`),
212
+ );
213
+ process.exit(1);
214
+ }
215
+
216
+ remoteConfig = (await res.json()) as RemoteConfig;
217
+ spinner.succeed("Remote config fetched");
218
+ } catch (error: unknown) {
219
+ spinner.fail("Failed to reach Revstack Cloud");
220
+ console.error(chalk.red(`\n ${(error as Error).message}\n`));
221
+ process.exit(1);
222
+ }
223
+
224
+ // ── 2. Show summary ────────────────────────────────────
225
+ const featureCount = Object.keys(remoteConfig.features).length;
226
+ const planCount = Object.keys(remoteConfig.plans).length;
227
+
228
+ console.log(
229
+ "\n" +
230
+ chalk.dim(" Remote state: ") +
231
+ chalk.white(`${featureCount} features, ${planCount} plans`) +
232
+ chalk.dim(` (${options.env})\n`),
233
+ );
234
+
235
+ // ── 3. Confirm overwrite ───────────────────────────────
236
+ const cwd = process.cwd();
237
+ const configPath = path.resolve(cwd, "revstack.config.ts");
238
+ const revstackDir = path.resolve(cwd, "revstack");
239
+ const featuresPath = path.resolve(revstackDir, "features.ts");
240
+ const plansPath = path.resolve(revstackDir, "plans.ts");
241
+
242
+ const rootExists = fs.existsSync(configPath);
243
+ const dirExists = fs.existsSync(revstackDir);
244
+
245
+ if (rootExists || dirExists) {
246
+ const { confirm } = await prompts({
247
+ type: "confirm",
248
+ name: "confirm",
249
+ message:
250
+ "This will overwrite your local configuration files (revstack.config.ts and revstack/ data). Are you sure?",
251
+ initial: false,
252
+ });
253
+
254
+ if (!confirm) {
255
+ console.log(chalk.dim("\n Pull cancelled.\n"));
256
+ return;
257
+ }
258
+ }
259
+
260
+ // ── 4. Generate and write ──────────────────────────────
261
+ if (!fs.existsSync(revstackDir)) {
262
+ fs.mkdirSync(revstackDir, { recursive: true });
263
+ }
264
+
265
+ const featuresSource = generateFeaturesSource(remoteConfig);
266
+ const plansSource = generatePlansSource(remoteConfig);
267
+ const rootSource = generateRootConfigSource();
268
+
269
+ fs.writeFileSync(featuresPath, featuresSource, "utf-8");
270
+ fs.writeFileSync(plansPath, plansSource, "utf-8");
271
+ fs.writeFileSync(configPath, rootSource, "utf-8");
272
+
273
+ console.log(
274
+ "\n" +
275
+ chalk.green(" ✔ Local files updated from remote.\n") +
276
+ chalk.dim(" Review the files and run ") +
277
+ chalk.bold("revstack push") +
278
+ chalk.dim(" to re-deploy.\n"),
279
+ );
280
+ });