@revstackhq/cli 0.0.0-dev-20260226055348 → 0.0.0-dev-20260226061807
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +32 -8
- package/CHANGELOG.md +6 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +95 -61
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +146 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +11 -0
- package/dist/commands/login.js +49 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +10 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/pull.d.ts +12 -0
- package/dist/commands/pull.js +194 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +12 -0
- package/dist/commands/push.js +192 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/utils/auth.d.ts +20 -0
- package/dist/utils/auth.js +42 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/config-loader.d.ts +15 -0
- package/dist/utils/config-loader.js +33 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/package.json +8 -1
- package/src/commands/init.ts +61 -46
- package/src/commands/pull.ts +51 -37
- package/tests/integration/init.test.ts +36 -3
- package/tests/integration/pull.test.ts +25 -5
- package/tsup.config.ts +8 -2
package/src/commands/pull.ts
CHANGED
|
@@ -52,19 +52,9 @@ interface RemoteConfig {
|
|
|
52
52
|
plans: Record<string, RemotePlan>;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// ─── Code Generator ──────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
function indent(text: string, spaces: number): string {
|
|
58
|
-
const pad = " ".repeat(spaces);
|
|
59
|
-
return text
|
|
60
|
-
.split("\n")
|
|
61
|
-
.map((line) => (line.trim() ? pad + line : line))
|
|
62
|
-
.join("\n");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
55
|
function serializeObject(
|
|
66
56
|
obj: Record<string, unknown>,
|
|
67
|
-
depth: number = 0
|
|
57
|
+
depth: number = 0,
|
|
68
58
|
): string {
|
|
69
59
|
const entries = Object.entries(obj);
|
|
70
60
|
if (entries.length === 0) return "{}";
|
|
@@ -110,8 +100,7 @@ function serializeArray(arr: unknown[], depth: number): string {
|
|
|
110
100
|
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
111
101
|
}
|
|
112
102
|
|
|
113
|
-
function
|
|
114
|
-
// ── Features ─────────────────────────────────────────────
|
|
103
|
+
function generateFeaturesSource(config: RemoteConfig): string {
|
|
115
104
|
const featureEntries = Object.entries(config.features).map(([slug, f]) => {
|
|
116
105
|
const props: Record<string, unknown> = {
|
|
117
106
|
name: f.name,
|
|
@@ -123,7 +112,15 @@ function generateConfigSource(config: RemoteConfig): string {
|
|
|
123
112
|
return ` ${slug}: defineFeature(${serializeObject(props, 2)}),`;
|
|
124
113
|
});
|
|
125
114
|
|
|
126
|
-
|
|
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 {
|
|
127
124
|
const planEntries = Object.entries(config.plans).map(([slug, plan]) => {
|
|
128
125
|
const props: Record<string, unknown> = {
|
|
129
126
|
name: plan.name,
|
|
@@ -146,21 +143,23 @@ function generateConfigSource(config: RemoteConfig): string {
|
|
|
146
143
|
return `${comment} ${slug}: definePlan<typeof features>(${serializeObject(props, 3)}),`;
|
|
147
144
|
});
|
|
148
145
|
|
|
149
|
-
return `import {
|
|
150
|
-
|
|
151
|
-
// ─── Features ────────────────────────────────────────────────
|
|
146
|
+
return `import { definePlan } from "@revstackhq/core";
|
|
147
|
+
import { features } from "./features";
|
|
152
148
|
|
|
153
|
-
const
|
|
154
|
-
${
|
|
149
|
+
export const plans = {
|
|
150
|
+
${planEntries.join("\n")}
|
|
155
151
|
};
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
156
154
|
|
|
157
|
-
|
|
155
|
+
function generateRootConfigSource(): string {
|
|
156
|
+
return `import { defineConfig } from "@revstackhq/core";
|
|
157
|
+
import { features } from "./revstack/features";
|
|
158
|
+
import { plans } from "./revstack/plans";
|
|
158
159
|
|
|
159
160
|
export default defineConfig({
|
|
160
161
|
features,
|
|
161
|
-
plans
|
|
162
|
-
${planEntries.join("\n")}
|
|
163
|
-
},
|
|
162
|
+
plans,
|
|
164
163
|
});
|
|
165
164
|
`;
|
|
166
165
|
}
|
|
@@ -173,7 +172,7 @@ const API_BASE = "https://app.revstack.dev";
|
|
|
173
172
|
|
|
174
173
|
export const pullCommand = new Command("pull")
|
|
175
174
|
.description(
|
|
176
|
-
"Pull the remote billing config and overwrite revstack.config.ts"
|
|
175
|
+
"Pull the remote billing config and overwrite local revstack.config.ts and revstack/ files",
|
|
177
176
|
)
|
|
178
177
|
.option("-e, --env <environment>", "Target environment", "test")
|
|
179
178
|
.action(async (options: { env: string }) => {
|
|
@@ -185,7 +184,7 @@ export const pullCommand = new Command("pull")
|
|
|
185
184
|
chalk.red(" ✖ Not authenticated.\n") +
|
|
186
185
|
chalk.dim(" Run ") +
|
|
187
186
|
chalk.bold("revstack login") +
|
|
188
|
-
chalk.dim(" first.\n")
|
|
187
|
+
chalk.dim(" first.\n"),
|
|
189
188
|
);
|
|
190
189
|
process.exit(1);
|
|
191
190
|
}
|
|
@@ -203,13 +202,13 @@ export const pullCommand = new Command("pull")
|
|
|
203
202
|
`${API_BASE}/api/v1/cli/pull?env=${encodeURIComponent(options.env)}`,
|
|
204
203
|
{
|
|
205
204
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
206
|
-
}
|
|
205
|
+
},
|
|
207
206
|
);
|
|
208
207
|
|
|
209
208
|
if (!res.ok) {
|
|
210
209
|
spinner.fail("Failed to fetch remote config");
|
|
211
210
|
console.error(
|
|
212
|
-
chalk.red(`\n API returned ${res.status}: ${res.statusText}\n`)
|
|
211
|
+
chalk.red(`\n API returned ${res.status}: ${res.statusText}\n`),
|
|
213
212
|
);
|
|
214
213
|
process.exit(1);
|
|
215
214
|
}
|
|
@@ -230,19 +229,25 @@ export const pullCommand = new Command("pull")
|
|
|
230
229
|
"\n" +
|
|
231
230
|
chalk.dim(" Remote state: ") +
|
|
232
231
|
chalk.white(`${featureCount} features, ${planCount} plans`) +
|
|
233
|
-
chalk.dim(` (${options.env})\n`)
|
|
232
|
+
chalk.dim(` (${options.env})\n`),
|
|
234
233
|
);
|
|
235
234
|
|
|
236
235
|
// ── 3. Confirm overwrite ───────────────────────────────
|
|
237
|
-
const
|
|
238
|
-
const
|
|
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");
|
|
239
241
|
|
|
240
|
-
|
|
242
|
+
const rootExists = fs.existsSync(configPath);
|
|
243
|
+
const dirExists = fs.existsSync(revstackDir);
|
|
244
|
+
|
|
245
|
+
if (rootExists || dirExists) {
|
|
241
246
|
const { confirm } = await prompts({
|
|
242
247
|
type: "confirm",
|
|
243
248
|
name: "confirm",
|
|
244
249
|
message:
|
|
245
|
-
"This will overwrite your local revstack.config.ts. Are you sure?",
|
|
250
|
+
"This will overwrite your local configuration files (revstack.config.ts and revstack/ data). Are you sure?",
|
|
246
251
|
initial: false,
|
|
247
252
|
});
|
|
248
253
|
|
|
@@ -253,14 +258,23 @@ export const pullCommand = new Command("pull")
|
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
// ── 4. Generate and write ──────────────────────────────
|
|
256
|
-
|
|
257
|
-
|
|
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");
|
|
258
272
|
|
|
259
273
|
console.log(
|
|
260
274
|
"\n" +
|
|
261
|
-
chalk.green(" ✔
|
|
262
|
-
chalk.dim(" Review the
|
|
275
|
+
chalk.green(" ✔ Local files updated from remote.\n") +
|
|
276
|
+
chalk.dim(" Review the files and run ") +
|
|
263
277
|
chalk.bold("revstack push") +
|
|
264
|
-
chalk.dim(" to re-deploy.\n")
|
|
278
|
+
chalk.dim(" to re-deploy.\n"),
|
|
265
279
|
);
|
|
266
280
|
});
|
|
@@ -6,9 +6,27 @@ vi.mock("node:fs", () => ({
|
|
|
6
6
|
default: {
|
|
7
7
|
existsSync: vi.fn(),
|
|
8
8
|
writeFileSync: vi.fn(),
|
|
9
|
+
mkdirSync: vi.fn(),
|
|
9
10
|
},
|
|
10
11
|
existsSync: vi.fn(),
|
|
11
12
|
writeFileSync: vi.fn(),
|
|
13
|
+
mkdirSync: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// ── Mock node:child_process ────────────────────────────────────
|
|
17
|
+
vi.mock("node:child_process", () => ({
|
|
18
|
+
spawnSync: vi.fn().mockReturnValue({ status: 0, error: null }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// ── Mock ora ─────────────────────────────────────────────────
|
|
22
|
+
const mockSpinner = {
|
|
23
|
+
start: vi.fn().mockReturnThis(),
|
|
24
|
+
succeed: vi.fn().mockReturnThis(),
|
|
25
|
+
fail: vi.fn().mockReturnThis(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
vi.mock("ora", () => ({
|
|
29
|
+
default: vi.fn(() => mockSpinner),
|
|
12
30
|
}));
|
|
13
31
|
|
|
14
32
|
// ── Mock chalk (no-op proxy) ─────────────────────────────────
|
|
@@ -67,13 +85,28 @@ describe("init command", () => {
|
|
|
67
85
|
const program = createTestProgram();
|
|
68
86
|
await program.parseAsync(["node", "revstack", "init"], { from: "node" });
|
|
69
87
|
|
|
88
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(
|
|
89
|
+
expect.stringContaining("revstack"),
|
|
90
|
+
{ recursive: true },
|
|
91
|
+
);
|
|
92
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(3);
|
|
93
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
94
|
+
expect.stringContaining("features.ts"),
|
|
95
|
+
expect.stringContaining("defineFeature"),
|
|
96
|
+
"utf-8",
|
|
97
|
+
);
|
|
98
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining("plans.ts"),
|
|
100
|
+
expect.stringContaining("definePlan"),
|
|
101
|
+
"utf-8",
|
|
102
|
+
);
|
|
70
103
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
71
104
|
expect.stringContaining("revstack.config.ts"),
|
|
72
105
|
expect.stringContaining("defineConfig"),
|
|
73
|
-
"utf-8"
|
|
106
|
+
"utf-8",
|
|
74
107
|
);
|
|
75
108
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
76
|
-
expect.stringContaining("Created revstack
|
|
109
|
+
expect.stringContaining("Created revstack config structure"),
|
|
77
110
|
);
|
|
78
111
|
|
|
79
112
|
consoleSpy.mockRestore();
|
|
@@ -86,7 +119,7 @@ describe("init command", () => {
|
|
|
86
119
|
const program = createTestProgram();
|
|
87
120
|
|
|
88
121
|
await expect(
|
|
89
|
-
program.parseAsync(["node", "revstack", "init"], { from: "node" })
|
|
122
|
+
program.parseAsync(["node", "revstack", "init"], { from: "node" }),
|
|
90
123
|
).rejects.toThrow(ProcessExitError);
|
|
91
124
|
|
|
92
125
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -37,9 +37,11 @@ vi.mock("node:fs", () => ({
|
|
|
37
37
|
default: {
|
|
38
38
|
existsSync: vi.fn(),
|
|
39
39
|
writeFileSync: vi.fn(),
|
|
40
|
+
mkdirSync: vi.fn(),
|
|
40
41
|
},
|
|
41
42
|
existsSync: vi.fn(),
|
|
42
43
|
writeFileSync: vi.fn(),
|
|
44
|
+
mkdirSync: vi.fn(),
|
|
43
45
|
}));
|
|
44
46
|
|
|
45
47
|
// ── Imports (after mocks) ────────────────────────────────────
|
|
@@ -130,7 +132,7 @@ describe("pull command", () => {
|
|
|
130
132
|
const program = createTestProgram();
|
|
131
133
|
|
|
132
134
|
await expect(
|
|
133
|
-
program.parseAsync(["node", "revstack", "pull"], { from: "node" })
|
|
135
|
+
program.parseAsync(["node", "revstack", "pull"], { from: "node" }),
|
|
134
136
|
).rejects.toThrow(ProcessExitError);
|
|
135
137
|
|
|
136
138
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -148,7 +150,7 @@ describe("pull command", () => {
|
|
|
148
150
|
const program = createTestProgram();
|
|
149
151
|
|
|
150
152
|
await expect(
|
|
151
|
-
program.parseAsync(["node", "revstack", "pull"], { from: "node" })
|
|
153
|
+
program.parseAsync(["node", "revstack", "pull"], { from: "node" }),
|
|
152
154
|
).rejects.toThrow(ProcessExitError);
|
|
153
155
|
|
|
154
156
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -157,7 +159,9 @@ describe("pull command", () => {
|
|
|
157
159
|
|
|
158
160
|
it("pulls config and writes file after user confirmation", async () => {
|
|
159
161
|
mockGetApiKey.mockReturnValue("sk_test_valid123");
|
|
160
|
-
mockFs.existsSync.
|
|
162
|
+
mockFs.existsSync.mockImplementation((p) =>
|
|
163
|
+
String(p).endsWith("revstack.config.ts"),
|
|
164
|
+
); // config file exists
|
|
161
165
|
|
|
162
166
|
const mockFetch = vi
|
|
163
167
|
.fn()
|
|
@@ -171,10 +175,25 @@ describe("pull command", () => {
|
|
|
171
175
|
|
|
172
176
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
173
177
|
expect(mockFetch.mock.calls[0][0]).toContain("/api/v1/cli/pull");
|
|
178
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining("revstack"),
|
|
180
|
+
{ recursive: true },
|
|
181
|
+
);
|
|
182
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(3);
|
|
183
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
184
|
+
expect.stringContaining("features.ts"),
|
|
185
|
+
expect.stringContaining("defineFeature"),
|
|
186
|
+
"utf-8",
|
|
187
|
+
);
|
|
188
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining("plans.ts"),
|
|
190
|
+
expect.stringContaining("definePlan"),
|
|
191
|
+
"utf-8",
|
|
192
|
+
);
|
|
174
193
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
175
194
|
expect.stringContaining("revstack.config.ts"),
|
|
176
195
|
expect.stringContaining("defineConfig"),
|
|
177
|
-
"utf-8"
|
|
196
|
+
"utf-8",
|
|
178
197
|
);
|
|
179
198
|
});
|
|
180
199
|
|
|
@@ -193,6 +212,7 @@ describe("pull command", () => {
|
|
|
193
212
|
await program.parseAsync(["node", "revstack", "pull"], { from: "node" });
|
|
194
213
|
|
|
195
214
|
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
|
|
215
|
+
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
196
216
|
});
|
|
197
217
|
|
|
198
218
|
it("skips confirmation when config file does not exist", async () => {
|
|
@@ -211,6 +231,6 @@ describe("pull command", () => {
|
|
|
211
231
|
// Prompts never called since file doesn't exist
|
|
212
232
|
expect(mockPrompts).not.toHaveBeenCalled();
|
|
213
233
|
// File was written directly
|
|
214
|
-
expect(mockFs.writeFileSync).
|
|
234
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(3);
|
|
215
235
|
});
|
|
216
236
|
});
|
package/tsup.config.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineConfig } from "tsup";
|
|
2
|
+
import { execa } from "execa";
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
4
|
-
entry: ["src
|
|
5
|
+
entry: ["src/**/*.ts"],
|
|
5
6
|
format: ["esm"],
|
|
6
|
-
dts:
|
|
7
|
+
dts: true,
|
|
7
8
|
splitting: false,
|
|
8
9
|
sourcemap: true,
|
|
9
10
|
clean: true,
|
|
@@ -13,4 +14,9 @@ export default defineConfig({
|
|
|
13
14
|
banner: {
|
|
14
15
|
js: "#!/usr/bin/env node",
|
|
15
16
|
},
|
|
17
|
+
async onSuccess() {
|
|
18
|
+
await execa("tsc-alias", ["-p", "tsconfig.json"], {
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
});
|
|
21
|
+
},
|
|
16
22
|
});
|