@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.
- package/.turbo/turbo-build.log +42 -30
- package/CHANGELOG.md +18 -0
- package/README.md +58 -38
- package/dist/cli.js +314 -43
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.js +279 -42
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +3 -1
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.js +32 -0
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/templates/b2b-saas.d.ts +5 -0
- package/dist/commands/templates/b2b-saas.js +104 -0
- package/dist/commands/templates/b2b-saas.js.map +1 -0
- package/dist/commands/templates/index.d.ts +5 -0
- package/dist/commands/templates/index.js +264 -0
- package/dist/commands/templates/index.js.map +1 -0
- package/dist/commands/templates/starter.d.ts +10 -0
- package/dist/commands/templates/starter.js +88 -0
- package/dist/commands/templates/starter.js.map +1 -0
- package/dist/commands/templates/usage-based.d.ts +5 -0
- package/dist/commands/templates/usage-based.js +75 -0
- package/dist/commands/templates/usage-based.js.map +1 -0
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/config-loader.js.map +1 -1
- package/package.json +3 -2
- package/src/cli.ts +32 -32
- package/src/commands/init.ts +187 -210
- package/src/commands/login.ts +39 -39
- package/src/commands/logout.ts +27 -25
- package/src/commands/pull.ts +280 -280
- package/src/commands/push.ts +244 -206
- package/src/commands/templates/b2b-saas.ts +99 -0
- package/src/commands/templates/index.ts +12 -0
- package/src/commands/templates/starter.ts +89 -0
- package/src/commands/templates/usage-based.ts +70 -0
- package/src/utils/auth.ts +59 -59
- package/src/utils/config-loader.ts +57 -57
- package/tests/integration/init.test.ts +12 -2
- package/tests/integration/push.test.ts +20 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TemplateConfig } from "./starter";
|
|
2
|
+
|
|
3
|
+
export const usageBased: TemplateConfig = {
|
|
4
|
+
features: `import { defineFeature } from "@revstackhq/core";
|
|
5
|
+
|
|
6
|
+
export const features = {
|
|
7
|
+
api_requests: defineFeature({ name: "API Requests", type: "metered", unit_type: "requests" }),
|
|
8
|
+
storage_gb: defineFeature({ name: "Storage (GB)", type: "metered", unit_type: "custom" }),
|
|
9
|
+
};
|
|
10
|
+
`,
|
|
11
|
+
addons: `import { defineAddon } from "@revstackhq/core";
|
|
12
|
+
import { features } from "./features";
|
|
13
|
+
|
|
14
|
+
export const addons = {
|
|
15
|
+
premium_support: defineAddon<typeof features>({
|
|
16
|
+
name: "Premium Support",
|
|
17
|
+
description: "24/7 dedicated support.",
|
|
18
|
+
type: "recurring",
|
|
19
|
+
prices: [
|
|
20
|
+
{ amount: 20000, currency: "USD", billing_interval: "monthly" }
|
|
21
|
+
],
|
|
22
|
+
features: {}
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
`,
|
|
26
|
+
plans: `import { definePlan } from "@revstackhq/core";
|
|
27
|
+
import { features } from "./features";
|
|
28
|
+
|
|
29
|
+
export const plans = {
|
|
30
|
+
default: definePlan<typeof features>({
|
|
31
|
+
name: "Default",
|
|
32
|
+
description: "Automatically created default plan for guests.",
|
|
33
|
+
is_default: true,
|
|
34
|
+
is_public: false,
|
|
35
|
+
type: "free",
|
|
36
|
+
features: {},
|
|
37
|
+
}),
|
|
38
|
+
pay_as_you_go: definePlan<typeof features>({
|
|
39
|
+
name: "Pay As You Go",
|
|
40
|
+
description: "Flexible usage-based pricing.",
|
|
41
|
+
is_default: false,
|
|
42
|
+
is_public: true,
|
|
43
|
+
type: "paid",
|
|
44
|
+
available_addons: ["premium_support"],
|
|
45
|
+
prices: [
|
|
46
|
+
{ amount: 0, currency: "USD", billing_interval: "monthly" } // Base platform fee
|
|
47
|
+
],
|
|
48
|
+
features: {
|
|
49
|
+
api_requests: { value_limit: 10000, is_hard_limit: false, reset_period: "monthly" }, // 10k free requests per month
|
|
50
|
+
storage_gb: { value_limit: 5, is_hard_limit: false, reset_period: "never" }, // 5GB free storage lifetime
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
`,
|
|
55
|
+
index: `import { defineConfig } from "@revstackhq/core";
|
|
56
|
+
import { features } from "./features";
|
|
57
|
+
import { addons } from "./addons";
|
|
58
|
+
import { plans } from "./plans";
|
|
59
|
+
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
features,
|
|
62
|
+
addons,
|
|
63
|
+
plans,
|
|
64
|
+
});
|
|
65
|
+
`,
|
|
66
|
+
root: `import config from "./revstack";
|
|
67
|
+
|
|
68
|
+
export default config;
|
|
69
|
+
`,
|
|
70
|
+
};
|
package/src/utils/auth.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
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
|
-
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
|
|
11
|
-
const CREDENTIALS_DIR = path.join(os.homedir(), ".revstack");
|
|
12
|
-
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
13
|
-
|
|
14
|
-
interface Credentials {
|
|
15
|
-
apiKey: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Persist an API key to the global credentials file.
|
|
20
|
-
* Creates `~/.revstack/` if it doesn't exist.
|
|
21
|
-
*/
|
|
22
|
-
export function setApiKey(key: string): void {
|
|
23
|
-
if (!fs.existsSync(CREDENTIALS_DIR)) {
|
|
24
|
-
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const credentials: Credentials = { apiKey: key };
|
|
28
|
-
fs.writeFileSync(
|
|
29
|
-
CREDENTIALS_FILE,
|
|
30
|
-
JSON.stringify(credentials, null, 2),
|
|
31
|
-
"utf-8"
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read the stored API key, or return `null` if none is configured.
|
|
37
|
-
*/
|
|
38
|
-
export function getApiKey(): string | null {
|
|
39
|
-
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const raw = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
45
|
-
const credentials: Credentials = JSON.parse(raw);
|
|
46
|
-
return credentials.apiKey ?? null;
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Remove stored credentials. Used by `revstack logout`.
|
|
54
|
-
*/
|
|
55
|
-
export function clearApiKey(): void {
|
|
56
|
-
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
57
|
-
fs.unlinkSync(CREDENTIALS_FILE);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
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
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
|
|
11
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), ".revstack");
|
|
12
|
+
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
13
|
+
|
|
14
|
+
interface Credentials {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Persist an API key to the global credentials file.
|
|
20
|
+
* Creates `~/.revstack/` if it doesn't exist.
|
|
21
|
+
*/
|
|
22
|
+
export function setApiKey(key: string): void {
|
|
23
|
+
if (!fs.existsSync(CREDENTIALS_DIR)) {
|
|
24
|
+
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const credentials: Credentials = { apiKey: key };
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
CREDENTIALS_FILE,
|
|
30
|
+
JSON.stringify(credentials, null, 2),
|
|
31
|
+
"utf-8",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the stored API key, or return `null` if none is configured.
|
|
37
|
+
*/
|
|
38
|
+
export function getApiKey(): string | null {
|
|
39
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
45
|
+
const credentials: Credentials = JSON.parse(raw);
|
|
46
|
+
return credentials.apiKey ?? null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove stored credentials. Used by `revstack logout`.
|
|
54
|
+
*/
|
|
55
|
+
export function clearApiKey(): void {
|
|
56
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
57
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
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
|
-
|
|
8
|
-
import { createJiti } from "jiti";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import chalk from "chalk";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Load the `revstack.config.ts` from the given directory.
|
|
14
|
-
*
|
|
15
|
-
* @param cwd - The directory to search for `revstack.config.ts`.
|
|
16
|
-
* @returns The parsed and sanitized configuration object.
|
|
17
|
-
*/
|
|
18
|
-
export async function loadLocalConfig(
|
|
19
|
-
cwd: string
|
|
20
|
-
): Promise<Record<string, unknown>> {
|
|
21
|
-
const configPath = path.resolve(cwd, "revstack.config.ts");
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const jiti = createJiti(cwd);
|
|
25
|
-
const module = (await jiti.import(configPath)) as Record<string, unknown>;
|
|
26
|
-
const config = (module.default ?? module) as Record<string, unknown>;
|
|
27
|
-
|
|
28
|
-
// Sanitize: strip functions, class instances, and non-serializable data.
|
|
29
|
-
// This ensures we only send plain JSON to the Revstack Cloud API.
|
|
30
|
-
return JSON.parse(JSON.stringify(config));
|
|
31
|
-
} catch (error: unknown) {
|
|
32
|
-
const err = error as NodeJS.ErrnoException;
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
err.code === "ERR_MODULE_NOT_FOUND" ||
|
|
36
|
-
err.code === "ENOENT" ||
|
|
37
|
-
err.code === "MODULE_NOT_FOUND"
|
|
38
|
-
) {
|
|
39
|
-
console.error(
|
|
40
|
-
chalk.red(
|
|
41
|
-
"\n ✖ Could not find revstack.config.ts in the current directory.\n"
|
|
42
|
-
) +
|
|
43
|
-
chalk.dim(" Run ") +
|
|
44
|
-
chalk.bold("revstack init") +
|
|
45
|
-
chalk.dim(" to create one.\n")
|
|
46
|
-
);
|
|
47
|
-
} else {
|
|
48
|
-
console.error(
|
|
49
|
-
chalk.red("\n ✖ Failed to parse revstack.config.ts\n") +
|
|
50
|
-
chalk.dim(" " + (err.message ?? String(error))) +
|
|
51
|
-
"\n"
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
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
|
+
|
|
8
|
+
import { createJiti } from "jiti";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load the `revstack.config.ts` from the given directory.
|
|
14
|
+
*
|
|
15
|
+
* @param cwd - The directory to search for `revstack.config.ts`.
|
|
16
|
+
* @returns The parsed and sanitized configuration object.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadLocalConfig(
|
|
19
|
+
cwd: string,
|
|
20
|
+
): Promise<Record<string, unknown>> {
|
|
21
|
+
const configPath = path.resolve(cwd, "revstack.config.ts");
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const jiti = createJiti(cwd);
|
|
25
|
+
const module = (await jiti.import(configPath)) as Record<string, unknown>;
|
|
26
|
+
const config = (module.default ?? module) as Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
// Sanitize: strip functions, class instances, and non-serializable data.
|
|
29
|
+
// This ensures we only send plain JSON to the Revstack Cloud API.
|
|
30
|
+
return JSON.parse(JSON.stringify(config));
|
|
31
|
+
} catch (error: unknown) {
|
|
32
|
+
const err = error as NodeJS.ErrnoException;
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
err.code === "ERR_MODULE_NOT_FOUND" ||
|
|
36
|
+
err.code === "ENOENT" ||
|
|
37
|
+
err.code === "MODULE_NOT_FOUND"
|
|
38
|
+
) {
|
|
39
|
+
console.error(
|
|
40
|
+
chalk.red(
|
|
41
|
+
"\n ✖ Could not find revstack.config.ts in the current directory.\n",
|
|
42
|
+
) +
|
|
43
|
+
chalk.dim(" Run ") +
|
|
44
|
+
chalk.bold("revstack init") +
|
|
45
|
+
chalk.dim(" to create one.\n"),
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
console.error(
|
|
49
|
+
chalk.red("\n ✖ Failed to parse revstack.config.ts\n") +
|
|
50
|
+
chalk.dim(" " + (err.message ?? String(error))) +
|
|
51
|
+
"\n",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -89,22 +89,32 @@ describe("init command", () => {
|
|
|
89
89
|
expect.stringContaining("revstack"),
|
|
90
90
|
{ recursive: true },
|
|
91
91
|
);
|
|
92
|
-
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(
|
|
92
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(5);
|
|
93
93
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
94
94
|
expect.stringContaining("features.ts"),
|
|
95
95
|
expect.stringContaining("defineFeature"),
|
|
96
96
|
"utf-8",
|
|
97
97
|
);
|
|
98
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining("addons.ts"),
|
|
100
|
+
expect.stringContaining("defineAddon"),
|
|
101
|
+
"utf-8",
|
|
102
|
+
);
|
|
98
103
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
99
104
|
expect.stringContaining("plans.ts"),
|
|
100
105
|
expect.stringContaining("definePlan"),
|
|
101
106
|
"utf-8",
|
|
102
107
|
);
|
|
103
108
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
104
|
-
expect.stringContaining("
|
|
109
|
+
expect.stringContaining("index.ts"),
|
|
105
110
|
expect.stringContaining("defineConfig"),
|
|
106
111
|
"utf-8",
|
|
107
112
|
);
|
|
113
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
114
|
+
expect.stringContaining("revstack.config.ts"),
|
|
115
|
+
expect.stringContaining("import config from"),
|
|
116
|
+
"utf-8",
|
|
117
|
+
);
|
|
108
118
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
109
119
|
expect.stringContaining("Created revstack config structure"),
|
|
110
120
|
);
|
|
@@ -16,6 +16,22 @@ vi.mock("prompts", () => ({
|
|
|
16
16
|
default: vi.fn(),
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
|
+
// ── Mock @revstackhq/core ────────────────────────────────────
|
|
20
|
+
vi.mock("@revstackhq/core", () => {
|
|
21
|
+
class RevstackValidationError extends Error {
|
|
22
|
+
errors: string[];
|
|
23
|
+
constructor(errors: string[]) {
|
|
24
|
+
super("Validation failed");
|
|
25
|
+
this.name = "RevstackValidationError";
|
|
26
|
+
this.errors = errors;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
validateConfig: vi.fn(),
|
|
31
|
+
RevstackValidationError,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
19
35
|
// ── Mock ora ─────────────────────────────────────────────────
|
|
20
36
|
const mockSpinner = {
|
|
21
37
|
start: vi.fn().mockReturnThis(),
|
|
@@ -129,7 +145,7 @@ describe("push command", () => {
|
|
|
129
145
|
const program = createTestProgram();
|
|
130
146
|
|
|
131
147
|
await expect(
|
|
132
|
-
program.parseAsync(["node", "revstack", "push"], { from: "node" })
|
|
148
|
+
program.parseAsync(["node", "revstack", "push"], { from: "node" }),
|
|
133
149
|
).rejects.toThrow(ProcessExitError);
|
|
134
150
|
|
|
135
151
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -139,7 +155,7 @@ describe("push command", () => {
|
|
|
139
155
|
it("diffs, confirms, and pushes successfully", async () => {
|
|
140
156
|
mockGetApiKey.mockReturnValue("sk_test_valid123");
|
|
141
157
|
mockLoadLocalConfig.mockResolvedValue(
|
|
142
|
-
SAMPLE_CONFIG as Record<string, unknown
|
|
158
|
+
SAMPLE_CONFIG as Record<string, unknown>,
|
|
143
159
|
);
|
|
144
160
|
|
|
145
161
|
const diffResponse = {
|
|
@@ -169,7 +185,7 @@ describe("push command", () => {
|
|
|
169
185
|
it("exits early when push is blocked (canPush: false)", async () => {
|
|
170
186
|
mockGetApiKey.mockReturnValue("sk_test_valid123");
|
|
171
187
|
mockLoadLocalConfig.mockResolvedValue(
|
|
172
|
-
SAMPLE_CONFIG as Record<string, unknown
|
|
188
|
+
SAMPLE_CONFIG as Record<string, unknown>,
|
|
173
189
|
);
|
|
174
190
|
|
|
175
191
|
const diffResponse = {
|
|
@@ -194,7 +210,7 @@ describe("push command", () => {
|
|
|
194
210
|
const program = createTestProgram();
|
|
195
211
|
|
|
196
212
|
await expect(
|
|
197
|
-
program.parseAsync(["node", "revstack", "push"], { from: "node" })
|
|
213
|
+
program.parseAsync(["node", "revstack", "push"], { from: "node" }),
|
|
198
214
|
).rejects.toThrow(ProcessExitError);
|
|
199
215
|
|
|
200
216
|
// Only diff called, push never reached
|