@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 CHANGED
@@ -6,13 +6,17 @@ You can choose how much you would like the CLI to automate for you– use it to
6
6
 
7
7
  ## Installation
8
8
 
9
- Try the quick install script (macOS/Linux):
9
+ ```bash
10
+ npm install -g @rulebricks/cli
11
+ ```
12
+
13
+ Alternatively, try the quick install script (macOS/Linux):
10
14
 
11
15
  ```bash
12
16
  curl -fsSL https://raw.githubusercontent.com/rulebricks/cli/main/install.sh | bash
13
17
  ```
14
18
 
15
- Standalone binaries are available on the [Releases page](https://github.com/rulebricks/cli/releases).
19
+ Standalone binaries are also available on the [Releases page](https://github.com/rulebricks/cli/releases).
16
20
 
17
21
  ## Prerequisites
18
22
 
@@ -41,7 +45,7 @@ rulebricks init
41
45
  rulebricks deploy my-deployment
42
46
  ```
43
47
 
44
- ## Commands
48
+ ## Main Commands
45
49
 
46
50
  | Command | Description |
47
51
  | --------------------------- | -------------------------------------- |
@@ -53,10 +57,10 @@ rulebricks deploy my-deployment
53
57
  | `rulebricks logs [name]` | Inspect services |
54
58
  | `rulebricks open [name]` | Open the generated configuration files |
55
59
 
56
- Add `-h` to any command to learn more about its options.
60
+ Use `rulebricks -h` to explore all commands, and add `-h` to any command to learn more about a particular command's options.
57
61
 
58
62
  ## Notes
59
63
 
60
64
  There are a uniquely wide variety of customization options this CLI makes available (multi-cloud, hybrid vs. self-hosted database deployment, custom email templates, etc.), and not all combinations have been validated.
61
65
 
62
- If you encounter any issue deploying your private Rulebricks cluster, please [email us](mailto:support@rulebricks.com) or [open an issue](https://github.com/rulebricks/cli/issues) and we will follow up promptly.
66
+ If you encounter any issue deploying your private Rulebricks cluster, please [email us](mailto:support@rulebricks.com) or [open an issue](https://github.com/rulebricks/cli/issues) and we will follow up promptly. If you are particularly familiar with helm/k8s, you are also free to review generated values.yaml files and reconcile them with our [Helm chart](https://github.com/rulebricks/helm).
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Benchmark Command
3
+ *
4
+ * Interactive wizard for configuring and running k6 load tests
5
+ * against Rulebricks deployments.
6
+ */
7
+ interface BenchmarkCommandProps {
8
+ name?: string;
9
+ }
10
+ export declare function BenchmarkCommand(props: BenchmarkCommandProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Benchmark Command
4
+ *
5
+ * Interactive wizard for configuring and running k6 load tests
6
+ * against Rulebricks deployments.
7
+ */
8
+ import { useState, useEffect, useCallback } from "react";
9
+ import { Box, Text, useApp, useInput } from "ink";
10
+ import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
11
+ import { DeploymentSelectStep, ApiKeyStep, FlowSlugStep, TestModeStep, PresetsStep, ReviewStep, createInitialBenchmarkState, } from "../components/Wizard/steps/BenchmarkSteps.js";
12
+ import { isK6Installed, getK6InstallInstructions, runBenchmark, buildApiUrl, openInBrowser, formatDuration, } from "../lib/benchmark.js";
13
+ function BenchmarkCommandInner({ name }) {
14
+ const { exit } = useApp();
15
+ const { colors } = useTheme();
16
+ const [step, setStep] = useState("preflight");
17
+ const [wizardState, setWizardState] = useState(createInitialBenchmarkState());
18
+ const [error, setError] = useState(null);
19
+ const [k6Output, setK6Output] = useState([]);
20
+ const [result, setResult] = useState(null);
21
+ // Preflight check for k6
22
+ useEffect(() => {
23
+ if (step !== "preflight")
24
+ return;
25
+ (async () => {
26
+ const installed = await isK6Installed();
27
+ if (!installed) {
28
+ setError(`k6 is not installed.\n\n${getK6InstallInstructions()}\n\nVisit https://k6.io/docs/get-started/installation/ for more options.`);
29
+ setStep("error");
30
+ return;
31
+ }
32
+ // If a deployment name was provided via CLI, skip the selection step
33
+ if (name) {
34
+ setWizardState((s) => ({ ...s, deploymentName: name }));
35
+ // We still need to load the deployment URL, which the DeploymentSelectStep does
36
+ // So we'll go to that step anyway to validate and load the URL
37
+ }
38
+ setStep("select-deployment");
39
+ })();
40
+ }, [step, name]);
41
+ // Handle wizard step completion
42
+ const handleStepComplete = useCallback((data) => {
43
+ setWizardState((s) => ({ ...s, ...data }));
44
+ // Progress to next step
45
+ switch (step) {
46
+ case "select-deployment":
47
+ setStep("api-key");
48
+ break;
49
+ case "api-key":
50
+ setStep("flow-slug");
51
+ break;
52
+ case "flow-slug":
53
+ setStep("test-mode");
54
+ break;
55
+ case "test-mode":
56
+ setStep("presets");
57
+ break;
58
+ case "presets":
59
+ setStep("review");
60
+ break;
61
+ case "review":
62
+ setStep("running");
63
+ break;
64
+ }
65
+ }, [step]);
66
+ // Handle going back
67
+ const handleBack = useCallback(() => {
68
+ switch (step) {
69
+ case "select-deployment":
70
+ exit();
71
+ break;
72
+ case "api-key":
73
+ setStep("select-deployment");
74
+ break;
75
+ case "flow-slug":
76
+ setStep("api-key");
77
+ break;
78
+ case "test-mode":
79
+ setStep("flow-slug");
80
+ break;
81
+ case "presets":
82
+ setStep("test-mode");
83
+ break;
84
+ case "review":
85
+ setStep("presets");
86
+ break;
87
+ }
88
+ }, [step, exit]);
89
+ // Run the benchmark
90
+ useEffect(() => {
91
+ if (step !== "running")
92
+ return;
93
+ (async () => {
94
+ const config = {
95
+ deploymentName: wizardState.deploymentName,
96
+ apiUrl: buildApiUrl(wizardState.deploymentUrl, wizardState.flowSlug),
97
+ apiKey: wizardState.apiKey,
98
+ testMode: wizardState.testMode,
99
+ testDuration: wizardState.testDuration,
100
+ targetRps: wizardState.targetRps,
101
+ bulkSize: wizardState.testMode === "throughput"
102
+ ? wizardState.bulkSize
103
+ : undefined,
104
+ };
105
+ const benchmarkResult = await runBenchmark(config, {
106
+ onOutput: (line) => {
107
+ setK6Output((prev) => {
108
+ // Keep only last 15 lines to avoid memory issues
109
+ const newOutput = [...prev, line];
110
+ if (newOutput.length > 15) {
111
+ return newOutput.slice(-15);
112
+ }
113
+ return newOutput;
114
+ });
115
+ },
116
+ });
117
+ setResult(benchmarkResult);
118
+ if (benchmarkResult.success) {
119
+ setStep("complete");
120
+ // Try to open report in browser
121
+ try {
122
+ await openInBrowser(benchmarkResult.reportPath);
123
+ }
124
+ catch {
125
+ // Ignore browser open errors
126
+ }
127
+ }
128
+ else {
129
+ setError(benchmarkResult.error || "Benchmark failed");
130
+ setStep("error");
131
+ }
132
+ })();
133
+ }, [step, wizardState]);
134
+ // Handle key input for error/complete screens
135
+ useInput((input, key) => {
136
+ if (key.escape && (step === "error" || step === "complete")) {
137
+ exit();
138
+ }
139
+ });
140
+ // Render preflight check
141
+ if (step === "preflight") {
142
+ return (_jsx(BorderBox, { title: "Benchmark", children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: "Checking prerequisites..." }) }) }));
143
+ }
144
+ // Render error screen
145
+ if (step === "error") {
146
+ return (_jsx(BorderBox, { title: "Benchmark Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "Error" }), _jsx(Text, { color: colors.error, children: error }), result?.outputDir && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Output directory: ", result.outputDir] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
147
+ }
148
+ // Render running screen
149
+ if (step === "running") {
150
+ const expectedThroughput = wizardState.testMode === "throughput"
151
+ ? wizardState.targetRps * wizardState.bulkSize
152
+ : wizardState.targetRps;
153
+ return (_jsx(BorderBox, { title: "Running Benchmark", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { label: `Running ${wizardState.testMode.toUpperCase()} test against ${wizardState.deploymentName}...` }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.muted, children: ["Target: ", expectedThroughput.toLocaleString(), " ", wizardState.testMode === "throughput" ? "solutions" : "requests", "/sec"] }), _jsxs(Text, { color: colors.muted, children: ["Duration: 1m warm-up + ", formatDuration(wizardState.testDuration)] })] }), k6Output.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: k6Output.map((line, i) => (_jsx(Text, { color: colors.muted, dimColor: true, children: line.length > 80 ? line.slice(0, 77) + "..." : line }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "This may take several minutes. Please wait..." }) })] }) }));
154
+ }
155
+ // Render complete screen
156
+ if (step === "complete" && result) {
157
+ const metrics = result.metrics;
158
+ return (_jsx(BorderBox, { title: "Benchmark Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Benchmark completed successfully!" }), metrics && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Results Summary:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Success Rate: " }), _jsxs(Text, { color: metrics.successRate >= 99
159
+ ? colors.success
160
+ : metrics.successRate >= 95
161
+ ? colors.warning
162
+ : colors.error, bold: true, children: [metrics.successRate.toFixed(1), "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Actual RPS: " }), _jsx(Text, { color: colors.accent, bold: true, children: metrics.actualRps.toFixed(1) })] }), metrics.actualThroughput && (_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Throughput: " }), _jsxs(Text, { color: colors.accent, bold: true, children: [metrics.actualThroughput.toFixed(0), " solutions/sec"] })] })), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P95 Latency: " }), _jsxs(Text, { color: metrics.p95Latency < 200
163
+ ? colors.success
164
+ : metrics.p95Latency < 500
165
+ ? colors.warning
166
+ : colors.error, children: [metrics.p95Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P99 Latency: " }), _jsxs(Text, { children: [metrics.p99Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Total Requests: " }), _jsx(Text, { children: metrics.totalRequests.toLocaleString() })] })] })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: "Results saved to:" }), _jsx(Text, { color: colors.accent, children: result.outputDir })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Report: ", result.reportPath.split("/").pop()] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "The HTML report should open in your browser automatically." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
167
+ }
168
+ // Render wizard steps
169
+ return (_jsxs(_Fragment, { children: [step === "select-deployment" && (_jsx(DeploymentSelectStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "api-key" && (_jsx(ApiKeyStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "flow-slug" && (_jsx(FlowSlugStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "test-mode" && (_jsx(TestModeStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "presets" && (_jsx(PresetsStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "review" && (_jsx(ReviewStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState }))] }));
170
+ }
171
+ export function BenchmarkCommand(props) {
172
+ return (_jsxs(ThemeProvider, { theme: "status", children: [_jsx(Logo, {}), _jsx(BenchmarkCommandInner, { ...props })] }));
173
+ }
@@ -5,7 +5,7 @@ import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "
5
5
  import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
6
6
  import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
7
7
  import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
8
- import { installChart, upgradeChart, isHelmInstalled } from "../lib/helm.js";
8
+ import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
9
9
  import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
10
10
  import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
11
11
  import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
@@ -194,7 +194,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
194
194
  // SINGLE-PHASE DEPLOYMENT (External DNS)
195
195
  // Install with TLS enabled from the start - external-dns handles DNS records
196
196
  await generateHelmValues(cfg, { tlsEnabled: true });
197
- await installChart(name, {
197
+ await installOrUpgradeChart(name, {
198
198
  releaseName,
199
199
  namespace,
200
200
  version,
@@ -223,7 +223,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
223
223
  // TWO-PHASE DEPLOYMENT (Manual DNS)
224
224
  // Phase 1: Install without TLS
225
225
  await generateHelmValues(cfg, { tlsEnabled: false });
226
- await installChart(name, {
226
+ await installOrUpgradeChart(name, {
227
227
  releaseName,
228
228
  namespace,
229
229
  version,
@@ -194,7 +194,7 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
194
194
  setError(err instanceof Error ? err.message : "Destruction failed");
195
195
  setStep("error");
196
196
  }
197
- }, [name, cluster, exit]);
197
+ }, [name, cluster, config, exit]);
198
198
  // Loading screen
199
199
  if (step === "loading") {
200
200
  return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Checking deployment state..." }) }) }));
@@ -243,7 +243,7 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
243
243
  // Only cleaning local files (with --config)
244
244
  _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2139 Local Cleanup" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: "No cluster resources found to clean up." }), _jsx(Text, { children: "This will delete local configuration files." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) })] })) : (
245
245
  // Full destruction
246
- _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !needsInfraConfirm && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Cloud infrastructure will be preserved. Use --cluster to remove it." }) })), !willDeleteConfig && (_jsx(Box, { marginTop: !needsInfraConfirm ? 0 : 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Local config files will be preserved. Use --config to remove them." }) })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "\u26A0 Cluster is not accessible. Cluster resources may need manual cleanup." }) }))] }), needsInfraConfirm ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Type", " ", _jsx(Text, { color: colors.accent, bold: true, children: "destroy-all" }), " ", "to confirm:"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(Text, { children: confirmText }), _jsx(Text, { color: colors.muted, children: "\u2588" })] })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) }))] })) }) }));
246
+ _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !cluster && scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Cloud infrastructure will be preserved. Use --cluster to remove it." }) })), cluster && !scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "No CLI managed infrastructure found for this deployment." }) })), !willDeleteConfig && (_jsx(Box, { marginTop: !needsInfraConfirm && !cluster ? 0 : 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Local config files will be preserved. Use --config to remove them." }) })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "\u26A0 Cluster is not accessible. Cluster resources may need manual cleanup." }) }))] }), needsInfraConfirm ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Type", " ", _jsx(Text, { color: colors.accent, bold: true, children: "destroy-all" }), " ", "to confirm:"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(Text, { children: confirmText }), _jsx(Text, { color: colors.muted, children: "\u2588" })] })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) }))] })) }) }));
247
247
  }
248
248
  export function DestroyCommand(props) {
249
249
  return (_jsxs(ThemeProvider, { theme: "destroy", children: [_jsx(Logo, {}), _jsx(DestroyCommandInner, { ...props })] }));
@@ -13,6 +13,7 @@ const COMPONENTS = [
13
13
  { label: "Kafka", value: "kafka" },
14
14
  { label: "Supabase", value: "supabase" },
15
15
  { label: "Traefik", value: "traefik" },
16
+ { label: "Redis", value: "redis" },
16
17
  ];
17
18
  /**
18
19
  * Shortens a pod name for display.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Benchmark Wizard Steps
3
+ *
4
+ * These components provide the interactive wizard flow for configuring
5
+ * and running benchmark tests against Rulebricks deployments.
6
+ */
7
+ import { BenchmarkTestMode, BenchmarkPreset } from "../../../types/index.js";
8
+ export interface BenchmarkWizardState {
9
+ deploymentName: string;
10
+ deploymentUrl: string;
11
+ apiKey: string;
12
+ flowSlug: string;
13
+ testMode: BenchmarkTestMode;
14
+ preset: BenchmarkPreset;
15
+ targetRps: number;
16
+ testDuration: string;
17
+ bulkSize: number;
18
+ }
19
+ interface StepProps {
20
+ onComplete: (data: Partial<BenchmarkWizardState>) => void;
21
+ onBack: () => void;
22
+ state: BenchmarkWizardState;
23
+ }
24
+ export declare function DeploymentSelectStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
25
+ export declare function ApiKeyStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function FlowSlugStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
27
+ export declare function TestModeStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function PresetsStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function ReviewStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
30
+ export declare function createInitialBenchmarkState(): BenchmarkWizardState;
31
+ export {};
@@ -0,0 +1,304 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Benchmark Wizard Steps
4
+ *
5
+ * These components provide the interactive wizard flow for configuring
6
+ * and running benchmark tests against Rulebricks deployments.
7
+ */
8
+ import { useState, useEffect } from "react";
9
+ import { Box, Text, useInput } from "ink";
10
+ import SelectInput from "ink-select-input";
11
+ import TextInput from "ink-text-input";
12
+ import { BorderBox, useTheme, Spinner } from "../../common/index.js";
13
+ import { QPS_PRESETS, THROUGHPUT_PRESETS, } from "../../../types/index.js";
14
+ import { listDeployments, loadDeploymentState } from "../../../lib/config.js";
15
+ import { buildApiUrl, checkDeploymentHealth } from "../../../lib/benchmark.js";
16
+ export function DeploymentSelectStep({ onComplete, onBack, state }) {
17
+ const { colors } = useTheme();
18
+ const [deployments, setDeployments] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [loadingStatus, setLoadingStatus] = useState("Loading deployments...");
21
+ const [error, setError] = useState(null);
22
+ useInput((input, key) => {
23
+ if (key.escape) {
24
+ onBack();
25
+ }
26
+ });
27
+ useEffect(() => {
28
+ (async () => {
29
+ try {
30
+ const names = await listDeployments();
31
+ const candidates = [];
32
+ // First, collect all deployments that have a URL
33
+ for (const name of names) {
34
+ try {
35
+ const deploymentState = await loadDeploymentState(name);
36
+ if (deploymentState?.application?.url) {
37
+ candidates.push({
38
+ name,
39
+ url: deploymentState.application.url,
40
+ });
41
+ }
42
+ }
43
+ catch {
44
+ // Skip deployments without state
45
+ }
46
+ }
47
+ if (candidates.length === 0) {
48
+ setError("No deployments found. Deploy a Rulebricks instance first with 'rulebricks deploy'.");
49
+ setLoading(false);
50
+ return;
51
+ }
52
+ // Now check health of each candidate
53
+ setLoadingStatus(`Checking health of ${candidates.length} deployment(s)...`);
54
+ const healthyDeployments = [];
55
+ for (const candidate of candidates) {
56
+ setLoadingStatus(`Checking ${candidate.name}...`);
57
+ const isHealthy = await checkDeploymentHealth(candidate.url);
58
+ if (isHealthy) {
59
+ healthyDeployments.push({
60
+ name: candidate.name,
61
+ url: candidate.url,
62
+ healthy: true,
63
+ });
64
+ }
65
+ }
66
+ setDeployments(healthyDeployments);
67
+ if (healthyDeployments.length === 0) {
68
+ setError("No healthy deployments found.\n\nAll configured deployments failed the health check (/api/health).\nMake sure your deployment is running and accessible.");
69
+ }
70
+ }
71
+ catch (err) {
72
+ setError("Failed to load deployments");
73
+ }
74
+ finally {
75
+ setLoading(false);
76
+ }
77
+ })();
78
+ }, []);
79
+ if (loading) {
80
+ return (_jsx(BorderBox, { title: "Select Deployment", children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: loadingStatus }) }) }));
81
+ }
82
+ if (error) {
83
+ return (_jsx(BorderBox, { title: "Select Deployment", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back" }) })] }) }));
84
+ }
85
+ const items = deployments.map((d) => ({
86
+ label: d.name,
87
+ value: d.name,
88
+ url: d.url,
89
+ }));
90
+ const handleSelect = (item) => {
91
+ const deployment = deployments.find((d) => d.name === item.value);
92
+ onComplete({
93
+ deploymentName: item.value,
94
+ deploymentUrl: deployment?.url || "",
95
+ });
96
+ };
97
+ return (_jsxs(BorderBox, { title: "Select Deployment", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Choose a deployment to benchmark:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Only healthy, accessible deployments are shown" })] }), _jsx(SelectInput, { items: items, onSelect: handleSelect, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => {
98
+ const deployment = deployments.find((d) => d.name === label);
99
+ return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && deployment?.url && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", deployment.url] }))] }));
100
+ } }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
101
+ }
102
+ // ============================================================================
103
+ // Step 2: API Key Input
104
+ // ============================================================================
105
+ export function ApiKeyStep({ onComplete, onBack, state }) {
106
+ const { colors } = useTheme();
107
+ const [apiKey, setApiKey] = useState(state.apiKey || "");
108
+ useInput((input, key) => {
109
+ if (key.escape) {
110
+ onBack();
111
+ }
112
+ });
113
+ const handleSubmit = () => {
114
+ if (!apiKey.trim())
115
+ return;
116
+ onComplete({ apiKey: apiKey.trim() });
117
+ };
118
+ return (_jsxs(BorderBox, { title: "API Key", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Rulebricks API key:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "This key is used to authenticate benchmark requests" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleSubmit, placeholder: "Enter your API key", mask: "*" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
119
+ }
120
+ // ============================================================================
121
+ // Step 3: Flow Slug Input
122
+ // ============================================================================
123
+ export function FlowSlugStep({ onComplete, onBack, state }) {
124
+ const { colors } = useTheme();
125
+ const [flowSlug, setFlowSlug] = useState(state.flowSlug || "");
126
+ useInput((input, key) => {
127
+ if (key.escape) {
128
+ onBack();
129
+ }
130
+ });
131
+ const handleSubmit = () => {
132
+ if (!flowSlug.trim())
133
+ return;
134
+ onComplete({ flowSlug: flowSlug.trim() });
135
+ };
136
+ return (_jsxs(BorderBox, { title: "Benchmarking Flow", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter the slug of your benchmarking flow:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Create a flow in Rulebricks using the \"Benchmarking Flow\" template, then find its slug (ex: \"oryoRqvOV1\")" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Expected payload schema:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 req_id: string (auto-generated)"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 alpha: number (0-100)"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 beta: string"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 charlie: boolean"] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: flowSlug, onChange: setFlowSlug, onSubmit: handleSubmit, placeholder: "e.g., benchmark-flow" })] }), state.deploymentUrl && flowSlug && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Will test: ", buildApiUrl(state.deploymentUrl, flowSlug)] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
137
+ }
138
+ // ============================================================================
139
+ // Step 4: Test Mode Selection
140
+ // ============================================================================
141
+ export function TestModeStep({ onComplete, onBack, state }) {
142
+ const { colors } = useTheme();
143
+ useInput((input, key) => {
144
+ if (key.escape) {
145
+ onBack();
146
+ }
147
+ });
148
+ const items = [
149
+ {
150
+ label: "QPS Test",
151
+ value: "qps",
152
+ description: "Measures requests per second - tests API responsiveness",
153
+ },
154
+ {
155
+ label: "Throughput Test",
156
+ value: "throughput",
157
+ description: "Measures solutions per second with bulk requests - tests engine capacity",
158
+ },
159
+ ];
160
+ const handleSelect = (item) => {
161
+ // Set default presets based on mode
162
+ const defaults = item.value === "qps"
163
+ ? {
164
+ targetRps: QPS_PRESETS.medium.targetRps,
165
+ testDuration: QPS_PRESETS.medium.testDuration,
166
+ }
167
+ : {
168
+ targetRps: THROUGHPUT_PRESETS.medium.targetRps,
169
+ testDuration: THROUGHPUT_PRESETS.medium.testDuration,
170
+ bulkSize: THROUGHPUT_PRESETS.medium.bulkSize,
171
+ };
172
+ onComplete({
173
+ testMode: item.value,
174
+ preset: "medium",
175
+ ...defaults,
176
+ });
177
+ };
178
+ return (_jsxs(BorderBox, { title: "Test Mode", children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { children: "Select the type of benchmark to run:" }) }), _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ({ isSelected, label }) => {
179
+ const item = items.find((i) => i.label === label);
180
+ return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && item && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", item.description] }))] }));
181
+ } }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
182
+ }
183
+ // ============================================================================
184
+ // Step 5: Presets Selection
185
+ // ============================================================================
186
+ export function PresetsStep({ onComplete, onBack, state }) {
187
+ const { colors } = useTheme();
188
+ const [customMode, setCustomMode] = useState(false);
189
+ const [customRps, setCustomRps] = useState(state.targetRps.toString());
190
+ const [customDuration, setCustomDuration] = useState(state.testDuration);
191
+ const [customBulkSize, setCustomBulkSize] = useState(state.bulkSize?.toString() || "50");
192
+ const [activeField, setActiveField] = useState("rps");
193
+ const presets = state.testMode === "qps" ? QPS_PRESETS : THROUGHPUT_PRESETS;
194
+ useInput((input, key) => {
195
+ if (key.escape) {
196
+ if (customMode) {
197
+ setCustomMode(false);
198
+ }
199
+ else {
200
+ onBack();
201
+ }
202
+ }
203
+ if (customMode && key.tab) {
204
+ // Cycle through fields
205
+ if (state.testMode === "throughput") {
206
+ setActiveField((prev) => prev === "rps"
207
+ ? "duration"
208
+ : prev === "duration"
209
+ ? "bulkSize"
210
+ : "rps");
211
+ }
212
+ else {
213
+ setActiveField((prev) => (prev === "rps" ? "duration" : "rps"));
214
+ }
215
+ }
216
+ });
217
+ const items = [
218
+ ...Object.entries(presets).map(([key, value]) => ({
219
+ label: value.label,
220
+ value: key,
221
+ description: value.description,
222
+ })),
223
+ {
224
+ label: "Custom",
225
+ value: "custom",
226
+ description: "Define your own test parameters",
227
+ },
228
+ ];
229
+ const handleSelect = (item) => {
230
+ if (item.value === "custom") {
231
+ setCustomMode(true);
232
+ return;
233
+ }
234
+ const preset = presets[item.value];
235
+ const data = {
236
+ preset: item.value,
237
+ targetRps: preset.targetRps,
238
+ testDuration: preset.testDuration,
239
+ };
240
+ if (state.testMode === "throughput" && "bulkSize" in preset) {
241
+ data.bulkSize = preset.bulkSize;
242
+ }
243
+ onComplete(data);
244
+ };
245
+ const handleCustomSubmit = () => {
246
+ const rps = parseInt(customRps, 10);
247
+ const bulkSize = parseInt(customBulkSize, 10);
248
+ if (isNaN(rps) || rps < 1)
249
+ return;
250
+ if (state.testMode === "throughput" && (isNaN(bulkSize) || bulkSize < 1))
251
+ return;
252
+ const data = {
253
+ preset: "custom",
254
+ targetRps: rps,
255
+ testDuration: customDuration,
256
+ };
257
+ if (state.testMode === "throughput") {
258
+ data.bulkSize = bulkSize;
259
+ }
260
+ onComplete(data);
261
+ };
262
+ if (customMode) {
263
+ return (_jsxs(BorderBox, { title: "Custom Configuration", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Configure your custom benchmark parameters:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: activeField === "rps" ? colors.accent : undefined, children: [activeField === "rps" ? "❯ " : " ", "Target RPS:", " "] }), activeField === "rps" ? (_jsx(TextInput, { value: customRps, onChange: setCustomRps, onSubmit: handleCustomSubmit, placeholder: "e.g., 500" })) : (_jsx(Text, { children: customRps }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: activeField === "duration" ? colors.accent : undefined, children: [activeField === "duration" ? "❯ " : " ", "Duration:", " "] }), activeField === "duration" ? (_jsx(TextInput, { value: customDuration, onChange: setCustomDuration, onSubmit: handleCustomSubmit, placeholder: "e.g., 4m" })) : (_jsx(Text, { children: customDuration }))] }), state.testMode === "throughput" && (_jsxs(Box, { children: [_jsxs(Text, { color: activeField === "bulkSize" ? colors.accent : undefined, children: [activeField === "bulkSize" ? "❯ " : " ", "Bulk Size:", " "] }), activeField === "bulkSize" ? (_jsx(TextInput, { value: customBulkSize, onChange: setCustomBulkSize, onSubmit: handleCustomSubmit, placeholder: "e.g., 50" })) : (_jsx(Text, { children: customBulkSize }))] }))] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Tab to switch fields \u2022 Esc to go back \u2022 Enter to continue" }) })] }));
264
+ }
265
+ return (_jsxs(BorderBox, { title: "Test Presets", children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsxs(Text, { children: ["Select a preset for your ", state.testMode.toUpperCase(), " test:"] }) }), _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ({ isSelected, label }) => {
266
+ const item = items.find((i) => i.label === label);
267
+ return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && item && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", item.description] }))] }));
268
+ } }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
269
+ }
270
+ // ============================================================================
271
+ // Step 6: Review and Confirm
272
+ // ============================================================================
273
+ export function ReviewStep({ onComplete, onBack, state }) {
274
+ const { colors } = useTheme();
275
+ useInput((input, key) => {
276
+ if (key.escape) {
277
+ onBack();
278
+ }
279
+ if (key.return) {
280
+ onComplete({});
281
+ }
282
+ });
283
+ const apiUrl = buildApiUrl(state.deploymentUrl, state.flowSlug);
284
+ const expectedThroughput = state.testMode === "throughput"
285
+ ? state.targetRps * state.bulkSize
286
+ : state.targetRps;
287
+ return (_jsxs(BorderBox, { title: "Review Configuration", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: "Ready to run benchmark" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Review your configuration before starting the test" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Deployment: " }), _jsx(Text, { bold: true, children: state.deploymentName })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Target URL: " }), _jsx(Text, { children: apiUrl })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Test Mode: " }), _jsx(Text, { bold: true, children: state.testMode.toUpperCase() })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Preset: " }), _jsx(Text, { children: state.preset })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Target RPS: " }), _jsxs(Text, { children: [state.targetRps, " requests/sec"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Duration: " }), _jsxs(Text, { children: [state.testDuration, " (+ 1m warm-up)"] })] }), state.testMode === "throughput" && (_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Bulk Size: " }), _jsxs(Text, { children: [state.bulkSize, " payloads/request"] })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.accent, children: ["Expected ", state.testMode === "throughput" ? "throughput" : "load", ": ~", expectedThroughput.toLocaleString(), " ", state.testMode === "throughput" ? "solutions" : "requests", "/sec"] }) })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to start benchmark" }) })] }));
288
+ }
289
+ // ============================================================================
290
+ // Initial State Factory
291
+ // ============================================================================
292
+ export function createInitialBenchmarkState() {
293
+ return {
294
+ deploymentName: "",
295
+ deploymentUrl: "",
296
+ apiKey: "",
297
+ flowSlug: "",
298
+ testMode: "qps",
299
+ preset: "medium",
300
+ targetRps: QPS_PRESETS.medium.targetRps,
301
+ testDuration: QPS_PRESETS.medium.testDuration,
302
+ bulkSize: THROUGHPUT_PRESETS.medium.bulkSize,
303
+ };
304
+ }