@rulebricks/cli 2.0.0 → 2.0.2

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 (38) hide show
  1. package/README.md +3 -3
  2. package/benchmarks/README.md +98 -0
  3. package/benchmarks/Test Flow.rbf +4088 -0
  4. package/benchmarks/benchmark-flow.json +26 -0
  5. package/benchmarks/lib/payload.js +101 -0
  6. package/benchmarks/lib/report.js +929 -0
  7. package/benchmarks/qps-test.js +136 -0
  8. package/benchmarks/run-qps-test.sh +115 -0
  9. package/benchmarks/run-throughput-test.sh +123 -0
  10. package/benchmarks/throughput-report.html +632 -0
  11. package/benchmarks/throughput-results.json +298 -0
  12. package/benchmarks/throughput-test.js +159 -0
  13. package/dist/commands/benchmark.d.ts +11 -0
  14. package/dist/commands/benchmark.js +173 -0
  15. package/dist/commands/deploy.js +15 -4
  16. package/dist/commands/destroy.js +2 -2
  17. package/dist/commands/logs.js +1 -0
  18. package/dist/components/Wizard/steps/BenchmarkSteps.d.ts +31 -0
  19. package/dist/components/Wizard/steps/BenchmarkSteps.js +304 -0
  20. package/dist/components/Wizard/steps/DatabaseStep.js +49 -35
  21. package/dist/index.js +42 -6
  22. package/dist/lib/benchmark.d.ts +63 -0
  23. package/dist/lib/benchmark.js +466 -0
  24. package/dist/lib/dns.d.ts +3 -1
  25. package/dist/lib/dns.js +138 -56
  26. package/dist/lib/helm.d.ts +14 -1
  27. package/dist/lib/helm.js +36 -1
  28. package/dist/lib/kubernetes.js +2 -0
  29. package/dist/types/index.d.ts +90 -0
  30. package/dist/types/index.js +51 -0
  31. package/package.json +8 -6
  32. package/terraform/aws/main.tf +22 -0
  33. package/terraform/azure/main.tf +45 -0
  34. package/terraform/gcp/main.tf +34 -0
  35. /package/{email-templates → templates}/email_change.html +0 -0
  36. /package/{email-templates → templates}/invite.html +0 -0
  37. /package/{email-templates → templates}/password_change.html +0 -0
  38. /package/{email-templates → templates}/verify.html +0 -0
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useCallback, useRef } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
+ import { platform } from "os";
4
5
  import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
5
6
  import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
6
7
  import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
7
8
  import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
8
- import { installChart, upgradeChart, isHelmInstalled } from "../lib/helm.js";
9
+ import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
9
10
  import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
10
11
  import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
11
12
  import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
@@ -194,7 +195,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
194
195
  // SINGLE-PHASE DEPLOYMENT (External DNS)
195
196
  // Install with TLS enabled from the start - external-dns handles DNS records
196
197
  await generateHelmValues(cfg, { tlsEnabled: true });
197
- await installChart(name, {
198
+ await installOrUpgradeChart(name, {
198
199
  releaseName,
199
200
  namespace,
200
201
  version,
@@ -223,7 +224,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
223
224
  // TWO-PHASE DEPLOYMENT (Manual DNS)
224
225
  // Phase 1: Install without TLS
225
226
  await generateHelmValues(cfg, { tlsEnabled: false });
226
- await installChart(name, {
227
+ await installOrUpgradeChart(name, {
227
228
  releaseName,
228
229
  namespace,
229
230
  version,
@@ -360,7 +361,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
360
361
  // Complete screen
361
362
  if (step === "complete") {
362
363
  const tlsSkipped = status.helmUpgradeTls === "skipped" && !useExternalDns;
363
- return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] })] }) }));
364
+ return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
364
365
  }
365
366
  // Progress screen
366
367
  const helmInstallLabel = useExternalDns
@@ -376,6 +377,16 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
376
377
  ? "Applying infrastructure"
377
378
  : undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
378
379
  }
380
+ function getDnsFlushCommand() {
381
+ switch (platform()) {
382
+ case "darwin":
383
+ return "sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder";
384
+ case "win32":
385
+ return "ipconfig /flushdns";
386
+ default:
387
+ return "sudo systemd-resolve --flush-caches";
388
+ }
389
+ }
379
390
  function getStepLabel(step, useExternalDns) {
380
391
  switch (step) {
381
392
  case "loading":
@@ -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, indicatorComponent: () => null, 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, indicatorComponent: () => null, 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
+ }
@@ -1,53 +1,58 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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';
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('type');
12
- const [supabaseUrl, setSupabaseUrl] = useState(state.supabaseUrl || '');
13
- const [anonKey, setAnonKey] = useState(state.supabaseAnonKey || '');
14
- const [serviceKey, setServiceKey] = useState(state.supabaseServiceKey || '');
15
- const [currentField, setCurrentField] = useState('anon');
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 === 'type') {
19
+ if (subStep === "type") {
19
20
  onBack();
20
21
  }
21
- else if (subStep === 'supabase-url') {
22
- setSubStep('type');
22
+ else if (subStep === "supabase-url") {
23
+ setSubStep("type");
23
24
  }
24
- else if (subStep === 'supabase-keys') {
25
- if (currentField === 'service') {
26
- setCurrentField('anon');
25
+ else if (subStep === "supabase-keys") {
26
+ if (currentField === "service") {
27
+ setCurrentField("anon");
27
28
  }
28
29
  else {
29
- setSubStep('supabase-url');
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: 'Self-hosted Supabase',
37
- value: 'self-hosted',
38
- description: 'Deploy Supabase as part of the Helm chart'
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: 'Supabase Cloud',
42
- value: 'supabase-cloud',
43
- description: 'Use your existing Supabase Cloud project'
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: 'SET_DATABASE_TYPE', dbType });
49
- if (dbType === 'supabase-cloud') {
50
- setSubStep('supabase-url');
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: 'SET_SUPABASE_CONFIG', config: { supabaseUrl } });
60
- setSubStep('supabase-keys');
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('service');
70
+ setCurrentField("service");
66
71
  };
67
72
  const handleServiceKeySubmit = () => {
68
73
  if (!serviceKey)
69
74
  return;
70
75
  dispatch({
71
- type: 'SET_SUPABASE_CONFIG',
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 === '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..." })] })] }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
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
  }