@rulebricks/cli 1.9.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/commands/benchmark.d.ts +11 -0
- package/dist/commands/benchmark.js +173 -0
- package/dist/commands/deploy.js +3 -3
- package/dist/commands/destroy.js +2 -2
- package/dist/commands/logs.js +1 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.d.ts +31 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.js +304 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +49 -35
- package/dist/index.js +42 -6
- package/dist/lib/benchmark.d.ts +63 -0
- package/dist/lib/benchmark.js +466 -0
- package/dist/lib/dns.d.ts +3 -1
- package/dist/lib/dns.js +138 -56
- package/dist/lib/helm.d.ts +14 -1
- package/dist/lib/helm.js +36 -1
- package/dist/lib/kubernetes.js +2 -0
- package/dist/types/index.d.ts +90 -0
- package/dist/types/index.js +51 -0
- package/package.json +5 -5
- package/terraform/aws/main.tf +22 -0
- package/terraform/azure/main.tf +45 -0
- package/terraform/gcp/main.tf +34 -0
- /package/{email-templates → templates}/email_change.html +0 -0
- /package/{email-templates → templates}/invite.html +0 -0
- /package/{email-templates → templates}/password_change.html +0 -0
- /package/{email-templates → templates}/verify.html +0 -0
|
@@ -1,53 +1,58 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from
|
|
3
|
-
import { Box, Text, useInput } from
|
|
4
|
-
import SelectInput from
|
|
5
|
-
import TextInput from
|
|
6
|
-
import { useWizard } from
|
|
7
|
-
import { BorderBox, useTheme } from
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import SelectInput from "ink-select-input";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import { useWizard } from "../WizardContext.js";
|
|
7
|
+
import { BorderBox, useTheme } from "../../common/index.js";
|
|
8
8
|
export function DatabaseStep({ onComplete, onBack }) {
|
|
9
9
|
const { state, dispatch } = useWizard();
|
|
10
10
|
const { colors } = useTheme();
|
|
11
|
-
const [subStep, setSubStep] = useState(
|
|
12
|
-
const [supabaseUrl, setSupabaseUrl] = useState(state.supabaseUrl ||
|
|
13
|
-
const [anonKey, setAnonKey] = useState(state.supabaseAnonKey ||
|
|
14
|
-
const [serviceKey, setServiceKey] = useState(state.supabaseServiceKey ||
|
|
15
|
-
const [
|
|
11
|
+
const [subStep, setSubStep] = useState("type");
|
|
12
|
+
const [supabaseUrl, setSupabaseUrl] = useState(state.supabaseUrl || "");
|
|
13
|
+
const [anonKey, setAnonKey] = useState(state.supabaseAnonKey || "");
|
|
14
|
+
const [serviceKey, setServiceKey] = useState(state.supabaseServiceKey || "");
|
|
15
|
+
const [accessToken, setAccessToken] = useState(state.supabaseAccessToken || "");
|
|
16
|
+
const [currentField, setCurrentField] = useState("anon");
|
|
16
17
|
useInput((input, key) => {
|
|
17
18
|
if (key.escape) {
|
|
18
|
-
if (subStep ===
|
|
19
|
+
if (subStep === "type") {
|
|
19
20
|
onBack();
|
|
20
21
|
}
|
|
21
|
-
else if (subStep ===
|
|
22
|
-
setSubStep(
|
|
22
|
+
else if (subStep === "supabase-url") {
|
|
23
|
+
setSubStep("type");
|
|
23
24
|
}
|
|
24
|
-
else if (subStep ===
|
|
25
|
-
if (currentField ===
|
|
26
|
-
setCurrentField(
|
|
25
|
+
else if (subStep === "supabase-keys") {
|
|
26
|
+
if (currentField === "service") {
|
|
27
|
+
setCurrentField("anon");
|
|
27
28
|
}
|
|
28
29
|
else {
|
|
29
|
-
setSubStep(
|
|
30
|
+
setSubStep("supabase-url");
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
else if (subStep === "access-token") {
|
|
34
|
+
setSubStep("supabase-keys");
|
|
35
|
+
setCurrentField("service");
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
});
|
|
34
39
|
const items = [
|
|
35
40
|
{
|
|
36
|
-
label:
|
|
37
|
-
value:
|
|
38
|
-
description:
|
|
41
|
+
label: "Self-hosted Supabase",
|
|
42
|
+
value: "self-hosted",
|
|
43
|
+
description: "Deploy Supabase as part of the Helm chart",
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
|
-
label:
|
|
42
|
-
value:
|
|
43
|
-
description:
|
|
44
|
-
}
|
|
46
|
+
label: "Supabase Cloud",
|
|
47
|
+
value: "supabase-cloud",
|
|
48
|
+
description: "Use your existing Supabase Cloud project",
|
|
49
|
+
},
|
|
45
50
|
];
|
|
46
51
|
const handleTypeSelect = (item) => {
|
|
47
52
|
const dbType = item.value;
|
|
48
|
-
dispatch({ type:
|
|
49
|
-
if (dbType ===
|
|
50
|
-
setSubStep(
|
|
53
|
+
dispatch({ type: "SET_DATABASE_TYPE", dbType });
|
|
54
|
+
if (dbType === "supabase-cloud") {
|
|
55
|
+
setSubStep("supabase-url");
|
|
51
56
|
}
|
|
52
57
|
else {
|
|
53
58
|
onComplete();
|
|
@@ -56,25 +61,34 @@ export function DatabaseStep({ onComplete, onBack }) {
|
|
|
56
61
|
const handleUrlSubmit = () => {
|
|
57
62
|
if (!supabaseUrl)
|
|
58
63
|
return;
|
|
59
|
-
dispatch({ type:
|
|
60
|
-
setSubStep(
|
|
64
|
+
dispatch({ type: "SET_SUPABASE_CONFIG", config: { supabaseUrl } });
|
|
65
|
+
setSubStep("supabase-keys");
|
|
61
66
|
};
|
|
62
67
|
const handleAnonKeySubmit = () => {
|
|
63
68
|
if (!anonKey)
|
|
64
69
|
return;
|
|
65
|
-
setCurrentField(
|
|
70
|
+
setCurrentField("service");
|
|
66
71
|
};
|
|
67
72
|
const handleServiceKeySubmit = () => {
|
|
68
73
|
if (!serviceKey)
|
|
69
74
|
return;
|
|
70
75
|
dispatch({
|
|
71
|
-
type:
|
|
76
|
+
type: "SET_SUPABASE_CONFIG",
|
|
72
77
|
config: {
|
|
73
78
|
supabaseAnonKey: anonKey,
|
|
74
|
-
supabaseServiceKey: serviceKey
|
|
75
|
-
}
|
|
79
|
+
supabaseServiceKey: serviceKey,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
setSubStep("access-token");
|
|
83
|
+
};
|
|
84
|
+
const handleAccessTokenSubmit = () => {
|
|
85
|
+
if (!accessToken)
|
|
86
|
+
return;
|
|
87
|
+
dispatch({
|
|
88
|
+
type: "SET_SUPABASE_CONFIG",
|
|
89
|
+
config: { supabaseAccessToken: accessToken },
|
|
76
90
|
});
|
|
77
91
|
onComplete();
|
|
78
92
|
};
|
|
79
|
-
return (_jsxs(BorderBox, { title: "Database", children: [subStep ===
|
|
93
|
+
return (_jsxs(BorderBox, { title: "Database", children: [subStep === "type" && (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { children: "Choose your database setup:" }) }), _jsx(SelectInput, { items: items, onSelect: handleTypeSelect, itemComponent: ({ isSelected, label }) => (_jsx(Text, { color: isSelected ? colors.accent : undefined, children: label })) })] })), subStep === "supabase-url" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Supabase project URL:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Find this in your Supabase Dashboard \u2192 Project Settings" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: supabaseUrl, onChange: setSupabaseUrl, onSubmit: handleUrlSubmit, placeholder: "https://xxxxx.supabase.co" })] })] })), subStep === "supabase-keys" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Supabase API keys:" }), currentField === "anon" ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Anon (public) key:" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: anonKey, onChange: setAnonKey, onSubmit: handleAnonKeySubmit, placeholder: "eyJhbGciOiJIUzI1NiIs..." })] })] })) : (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { children: [" Anon key: ", anonKey.substring(0, 20), "..."] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Service role key:" }) }), _jsxs(Box, { children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: serviceKey, onChange: setServiceKey, onSubmit: handleServiceKeySubmit, placeholder: "eyJhbGciOiJIUzI1NiIs..." })] })] }))] })), subStep === "access-token" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Supabase Access Token:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Find this in Supabase Dashboard \u2192 Account Settings \u2192 Access Tokens" }), _jsx(Text, { color: "gray", dimColor: true, children: "This is required for managing your Supabase project." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: accessToken, onChange: setAccessToken, onSubmit: handleAccessTokenSubmit, placeholder: "sbp_...", mask: "*" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { color: "gray", children: " Supabase URL configured" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { color: "gray", children: " API keys configured" })] })] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
80
94
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { StatusCommand } from "./commands/status.js";
|
|
|
11
11
|
import { LogsCommand } from "./commands/logs.js";
|
|
12
12
|
import { CloneCommand } from "./commands/clone.js";
|
|
13
13
|
import { OpenCommand } from "./commands/open.js";
|
|
14
|
+
import { BenchmarkCommand } from "./commands/benchmark.js";
|
|
14
15
|
import { listDeployments, deploymentExists } from "./lib/config.js";
|
|
15
16
|
const VERSION = "2.0.0";
|
|
16
17
|
const program = new Command();
|
|
@@ -58,10 +59,23 @@ program
|
|
|
58
59
|
.option("--version <version>", "Target version (defaults to latest)")
|
|
59
60
|
.option("--dry-run", "Preview changes without applying")
|
|
60
61
|
.action(async (name, options) => {
|
|
61
|
-
|
|
62
|
+
let deploymentName = name;
|
|
62
63
|
if (!deploymentName) {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const deployments = await listDeployments();
|
|
65
|
+
if (deployments.length === 0) {
|
|
66
|
+
console.error(chalk.red('No deployments found. Run "rulebricks init" first.'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
else if (deployments.length > 1) {
|
|
70
|
+
console.error(chalk.red("Please specify a deployment to upgrade.\n"));
|
|
71
|
+
console.log("Available deployments:");
|
|
72
|
+
for (const d of deployments) {
|
|
73
|
+
console.log(` ${chalk.yellow("•")} ${d}`);
|
|
74
|
+
}
|
|
75
|
+
console.log(`\nUsage: ${chalk.cyan("rulebricks upgrade <name>")}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
deploymentName = deployments[0]; // Only one deployment, auto-select
|
|
65
79
|
}
|
|
66
80
|
const { waitUntilExit } = render(_jsx(UpgradeCommand, { name: deploymentName, targetVersion: options.version, dryRun: options.dryRun }));
|
|
67
81
|
await waitUntilExit();
|
|
@@ -119,10 +133,23 @@ program
|
|
|
119
133
|
.option("-t, --tail <lines>", "Number of lines to show", "100")
|
|
120
134
|
.option("-s, --split", "Show logs in split-pane view (side-by-side columns)")
|
|
121
135
|
.action(async (name, component, options) => {
|
|
122
|
-
|
|
136
|
+
let deploymentName = name;
|
|
123
137
|
if (!deploymentName) {
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
const deployments = await listDeployments();
|
|
139
|
+
if (deployments.length === 0) {
|
|
140
|
+
console.error(chalk.red('No deployments found. Run "rulebricks init" first.'));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
else if (deployments.length > 1) {
|
|
144
|
+
console.error(chalk.red("Please specify a deployment to view logs for.\n"));
|
|
145
|
+
console.log("Available deployments:");
|
|
146
|
+
for (const d of deployments) {
|
|
147
|
+
console.log(` ${chalk.yellow("•")} ${d}`);
|
|
148
|
+
}
|
|
149
|
+
console.log(`\nUsage: ${chalk.cyan("rulebricks logs <name> [component]")}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
deploymentName = deployments[0]; // Only one deployment, auto-select
|
|
126
153
|
}
|
|
127
154
|
const { waitUntilExit } = render(_jsx(LogsCommand, { name: deploymentName, component: component, follow: options.follow, tail: parseInt(options.tail, 10), split: options.split }));
|
|
128
155
|
await waitUntilExit();
|
|
@@ -186,6 +213,15 @@ program
|
|
|
186
213
|
const { waitUntilExit } = render(_jsx(OpenCommand, { name: name, target: target }));
|
|
187
214
|
await waitUntilExit();
|
|
188
215
|
});
|
|
216
|
+
// Benchmark command
|
|
217
|
+
program
|
|
218
|
+
.command("benchmark")
|
|
219
|
+
.description("Run load tests against a Rulebricks deployment")
|
|
220
|
+
.argument("[name]", "Deployment name (optional)")
|
|
221
|
+
.action(async (name) => {
|
|
222
|
+
const { waitUntilExit } = render(_jsx(BenchmarkCommand, { name: name }));
|
|
223
|
+
await waitUntilExit();
|
|
224
|
+
});
|
|
189
225
|
// Helper to select a deployment interactively
|
|
190
226
|
async function selectDeployment() {
|
|
191
227
|
const deployments = await listDeployments();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark utilities for running k6 load tests against Rulebricks deployments
|
|
3
|
+
*/
|
|
4
|
+
import { BenchmarkConfig, BenchmarkResult } from "../types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Check if k6 is installed and available
|
|
7
|
+
*/
|
|
8
|
+
export declare function isK6Installed(): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Get k6 version string
|
|
11
|
+
*/
|
|
12
|
+
export declare function getK6Version(): Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Get k6 installation instructions based on OS
|
|
15
|
+
*/
|
|
16
|
+
export declare function getK6InstallInstructions(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a URL is a valid benchmark target (not a cloud URL)
|
|
19
|
+
*/
|
|
20
|
+
export declare function isValidBenchmarkTarget(url: string): {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Check if a deployment is healthy by calling the /api/health endpoint
|
|
26
|
+
* Returns true if the deployment responds with {"status":"OK"}
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkDeploymentHealth(deploymentUrl: string): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Build the full API URL from deployment domain and flow slug
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildApiUrl(domain: string, flowSlug: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Ensure benchmark scripts directory exists and contains the test scripts
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureBenchmarkScripts(): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Create the output directory for benchmark results
|
|
39
|
+
*/
|
|
40
|
+
export declare function createOutputDirectory(deploymentName: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Save benchmark configuration to output directory
|
|
43
|
+
*/
|
|
44
|
+
export declare function saveBenchmarkConfig(outputDir: string, config: BenchmarkConfig): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Run a benchmark test
|
|
47
|
+
*/
|
|
48
|
+
export declare function runBenchmark(config: BenchmarkConfig, options?: {
|
|
49
|
+
onOutput?: (line: string) => void;
|
|
50
|
+
onProgress?: (phase: string, progress: number) => void;
|
|
51
|
+
}): Promise<BenchmarkResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Open a file in the default browser/application
|
|
54
|
+
*/
|
|
55
|
+
export declare function openInBrowser(filePath: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Format duration string (e.g., "4m") to human readable
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatDuration(duration: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Calculate expected throughput for display
|
|
62
|
+
*/
|
|
63
|
+
export declare function calculateExpectedThroughput(targetRps: number, bulkSize?: number): number;
|