@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.
@@ -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
  }
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
- const deploymentName = name || (await selectDeployment());
62
+ let deploymentName = name;
62
63
  if (!deploymentName) {
63
- console.error(chalk.red("No deployment specified."));
64
- process.exit(1);
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
- const deploymentName = name || (await selectDeployment());
136
+ let deploymentName = name;
123
137
  if (!deploymentName) {
124
- console.error(chalk.red("No deployment specified."));
125
- process.exit(1);
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;