@rulebricks/cli 1.9.0

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 (93) hide show
  1. package/README.md +62 -0
  2. package/dist/commands/clone.d.ts +6 -0
  3. package/dist/commands/clone.js +60 -0
  4. package/dist/commands/deploy.d.ts +8 -0
  5. package/dist/commands/deploy.js +409 -0
  6. package/dist/commands/destroy.d.ts +8 -0
  7. package/dist/commands/destroy.js +298 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.js +201 -0
  10. package/dist/commands/logs.d.ts +9 -0
  11. package/dist/commands/logs.js +222 -0
  12. package/dist/commands/open.d.ts +7 -0
  13. package/dist/commands/open.js +139 -0
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +125 -0
  16. package/dist/commands/upgrade.d.ts +7 -0
  17. package/dist/commands/upgrade.js +239 -0
  18. package/dist/components/DNSWaitScreen.d.ts +9 -0
  19. package/dist/components/DNSWaitScreen.js +73 -0
  20. package/dist/components/Wizard/WizardContext.d.ts +176 -0
  21. package/dist/components/Wizard/WizardContext.js +346 -0
  22. package/dist/components/Wizard/index.d.ts +2 -0
  23. package/dist/components/Wizard/index.js +2 -0
  24. package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
  25. package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
  26. package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
  27. package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
  28. package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
  29. package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
  30. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
  31. package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
  32. package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
  33. package/dist/components/Wizard/steps/DomainStep.js +126 -0
  34. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
  35. package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
  36. package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
  38. package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
  39. package/dist/components/Wizard/steps/ReviewStep.js +56 -0
  40. package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
  41. package/dist/components/Wizard/steps/SMTPStep.js +191 -0
  42. package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
  43. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
  44. package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
  45. package/dist/components/Wizard/steps/TierStep.js +29 -0
  46. package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
  47. package/dist/components/Wizard/steps/VersionStep.js +113 -0
  48. package/dist/components/Wizard/steps/index.d.ts +12 -0
  49. package/dist/components/Wizard/steps/index.js +12 -0
  50. package/dist/components/common/AppShell.d.ts +31 -0
  51. package/dist/components/common/AppShell.js +31 -0
  52. package/dist/components/common/Box.d.ts +20 -0
  53. package/dist/components/common/Box.js +20 -0
  54. package/dist/components/common/Logo.d.ts +7 -0
  55. package/dist/components/common/Logo.js +22 -0
  56. package/dist/components/common/Spinner.d.ts +12 -0
  57. package/dist/components/common/Spinner.js +28 -0
  58. package/dist/components/common/index.d.ts +6 -0
  59. package/dist/components/common/index.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +202 -0
  62. package/dist/lib/cloudCli.d.ts +156 -0
  63. package/dist/lib/cloudCli.js +691 -0
  64. package/dist/lib/config.d.ts +91 -0
  65. package/dist/lib/config.js +278 -0
  66. package/dist/lib/dns.d.ts +41 -0
  67. package/dist/lib/dns.js +235 -0
  68. package/dist/lib/dockerHub.d.ts +57 -0
  69. package/dist/lib/dockerHub.js +128 -0
  70. package/dist/lib/helm.d.ts +53 -0
  71. package/dist/lib/helm.js +209 -0
  72. package/dist/lib/helmValues.d.ts +17 -0
  73. package/dist/lib/helmValues.js +693 -0
  74. package/dist/lib/kubernetes.d.ts +161 -0
  75. package/dist/lib/kubernetes.js +755 -0
  76. package/dist/lib/terraform.d.ts +44 -0
  77. package/dist/lib/terraform.js +230 -0
  78. package/dist/lib/theme.d.ts +81 -0
  79. package/dist/lib/theme.js +115 -0
  80. package/dist/lib/validation.d.ts +47 -0
  81. package/dist/lib/validation.js +164 -0
  82. package/dist/lib/versions.d.ts +69 -0
  83. package/dist/lib/versions.js +139 -0
  84. package/dist/types/index.d.ts +718 -0
  85. package/dist/types/index.js +556 -0
  86. package/email-templates/email_change.html +325 -0
  87. package/email-templates/invite.html +383 -0
  88. package/email-templates/password_change.html +414 -0
  89. package/email-templates/verify.html +396 -0
  90. package/package.json +78 -0
  91. package/terraform/aws/main.tf +327 -0
  92. package/terraform/azure/main.tf +326 -0
  93. package/terraform/gcp/main.tf +369 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ ![Banner](./banner.png)
2
+
3
+ The Rulebricks CLI is a management utility that automates the creation and maintenance of private Rulebricks clusters, helping you deploy Rulebricks in customizable, high-throughput configurations on AWS, GCP, or Azure.
4
+
5
+ You can choose how much you would like the CLI to automate for you– use it to generate valid configuration values, automate infrastructure provisioning (via Terraform), software deployment (via Helm), or all of the above.
6
+
7
+ ## Installation
8
+
9
+ Try the quick install script (macOS/Linux):
10
+
11
+ ```bash
12
+ curl -fsSL https://raw.githubusercontent.com/rulebricks/cli/main/install.sh | bash
13
+ ```
14
+
15
+ Standalone binaries are available on the [Releases page](https://github.com/rulebricks/cli/releases).
16
+
17
+ ## Prerequisites
18
+
19
+ You must have a valid **Rulebricks license key**
20
+ to deploy using this CLI. You will be
21
+ requested for this key during project
22
+ configuration.
23
+
24
+ Rulebricks requires TLS. You will require either external-dns on your cluster to automatically add DNS records, or you will need **access** to manually add **DNS records** for the subdomain(s) where you would like to access your private deployment from.
25
+
26
+ Finally, you will need to have the following tools installed and ready on your machine:
27
+
28
+ - **Node.js** >= 20
29
+ - **kubectl** - Kubernetes CLI
30
+ - **Helm** >= 3.0
31
+ - **Terraform** >= 1.0 (for infrastructure provisioning)
32
+ - Cloud CLI (`aws`, `gcloud`, or `az`) configured for your provider
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Configuration wizard (generates values.yaml)
38
+ rulebricks init
39
+
40
+ # Provision and/or deploy to your cluster
41
+ rulebricks deploy my-deployment
42
+ ```
43
+
44
+ ## Commands
45
+
46
+ | Command | Description |
47
+ | --------------------------- | -------------------------------------- |
48
+ | `rulebricks init` | Interactive setup wizard |
49
+ | `rulebricks deploy [name]` | Deploy to Kubernetes |
50
+ | `rulebricks upgrade [name]` | Upgrade to a new version |
51
+ | `rulebricks destroy [name]` | Remove a deployment |
52
+ | `rulebricks status [name]` | Show deployment health |
53
+ | `rulebricks logs [name]` | Inspect services |
54
+ | `rulebricks open [name]` | Open the generated configuration files |
55
+
56
+ Add `-h` to any command to learn more about its options.
57
+
58
+ ## Notes
59
+
60
+ 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
+
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.
@@ -0,0 +1,6 @@
1
+ interface CloneCommandProps {
2
+ source: string;
3
+ target: string;
4
+ }
5
+ export declare function CloneCommand(props: CloneCommandProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
5
+ import { deploymentExists, cloneDeploymentConfig, getDeploymentDir, } from "../lib/config.js";
6
+ import { generateHelmValues } from "../lib/helmValues.js";
7
+ function CloneCommandInner({ source, target }) {
8
+ const { exit } = useApp();
9
+ const { colors } = useTheme();
10
+ const [step, setStep] = useState("validating");
11
+ const [error, setError] = useState(null);
12
+ useEffect(() => {
13
+ (async () => {
14
+ try {
15
+ // Validate source exists
16
+ const sourceExists = await deploymentExists(source);
17
+ if (!sourceExists) {
18
+ setError(`Source deployment "${source}" not found`);
19
+ setStep("error");
20
+ return;
21
+ }
22
+ // Validate target doesn't exist
23
+ const targetExists = await deploymentExists(target);
24
+ if (targetExists) {
25
+ setError(`Target deployment "${target}" already exists`);
26
+ setStep("error");
27
+ return;
28
+ }
29
+ // Clone the configuration
30
+ setStep("cloning");
31
+ const clonedConfig = await cloneDeploymentConfig(source, target);
32
+ // Generate fresh Helm values from the cloned config
33
+ await generateHelmValues(clonedConfig);
34
+ setStep("complete");
35
+ setTimeout(() => exit(), 3000);
36
+ }
37
+ catch (err) {
38
+ setError(err instanceof Error ? err.message : "Failed to clone deployment");
39
+ setStep("error");
40
+ }
41
+ })();
42
+ }, [source, target, exit]);
43
+ // Validating screen
44
+ if (step === "validating") {
45
+ return (_jsx(BorderBox, { title: "Clone Deployment", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Validating deployments..." }) }) }));
46
+ }
47
+ // Error screen
48
+ if (step === "error") {
49
+ return (_jsx(BorderBox, { title: "Clone Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Error" }), _jsx(Text, { color: colors.error, children: error }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Use ", _jsx(Text, { color: colors.accent, children: "rulebricks list" }), " to see available deployments."] }) })] }) }));
50
+ }
51
+ // Cloning screen
52
+ if (step === "cloning") {
53
+ return (_jsx(BorderBox, { title: "Clone Deployment", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.muted, children: ["Source: ", _jsx(Text, { color: colors.accent, children: source })] }), _jsxs(Text, { color: colors.muted, children: ["Target: ", _jsx(Text, { color: colors.accent, children: target })] }), _jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Cloning configuration..." }) })] }) }));
54
+ }
55
+ // Complete screen
56
+ return (_jsx(BorderBox, { title: "Clone Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: ["\u2713 Successfully cloned \"", source, "\" to \"", target, "\""] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Created files:" }), _jsx(Text, { color: colors.muted, children: " \u2022 config.yaml" }), _jsx(Text, { color: colors.muted, children: " \u2022 values.yaml" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Location: ", getDeploymentDir(target)] }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Next steps:" }), _jsxs(Text, { color: colors.accent, children: [" rulebricks deploy ", target] })] })] }) }));
57
+ }
58
+ export function CloneCommand(props) {
59
+ return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(CloneCommandInner, { ...props })] }));
60
+ }
@@ -0,0 +1,8 @@
1
+ interface DeployCommandProps {
2
+ name: string;
3
+ skipInfra?: boolean;
4
+ skipDns?: boolean;
5
+ version?: string;
6
+ }
7
+ export declare function DeployCommand(props: DeployCommandProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,409 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useRef } from "react";
3
+ import { Box, Text, useApp, useInput } from "ink";
4
+ import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
5
+ import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
6
+ import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
7
+ import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
8
+ import { installChart, upgradeChart, isHelmInstalled } from "../lib/helm.js";
9
+ import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
10
+ import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
11
+ import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
12
+ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
13
+ const { exit } = useApp();
14
+ const { colors } = useTheme();
15
+ const [step, setStep] = useState("loading");
16
+ const [config, setConfig] = useState(null);
17
+ const [error, setError] = useState(null);
18
+ const [useExternalDns, setUseExternalDns] = useState(false);
19
+ const infraStartedRef = useRef(false); // Track if we started infra provisioning (ref for sync access)
20
+ const [cleanupError, setCleanupError] = useState(null);
21
+ const [status, setStatus] = useState({
22
+ preflight: "pending",
23
+ infrastructure: "pending",
24
+ kubeconfig: "pending",
25
+ helmInstall: "pending",
26
+ dnsConfig: "pending",
27
+ helmUpgradeTls: "pending",
28
+ });
29
+ // Handle cleanup prompt responses
30
+ const handleCleanup = useCallback(async () => {
31
+ setStep("cleanup-running");
32
+ try {
33
+ await terraformDestroy(name);
34
+ setStep("cleanup-complete");
35
+ setTimeout(() => exit(), 3000);
36
+ }
37
+ catch (err) {
38
+ setCleanupError(err instanceof Error ? err.message : "Cleanup failed");
39
+ setStep("cleanup-complete");
40
+ setTimeout(() => exit(), 5000);
41
+ }
42
+ }, [name, exit]);
43
+ const skipCleanup = useCallback(() => {
44
+ setStep("error");
45
+ }, []);
46
+ useInput((input, key) => {
47
+ if (step === "cleanup-prompt") {
48
+ if (input === "y" || input === "Y") {
49
+ handleCleanup();
50
+ }
51
+ else if (input === "n" || input === "N" || key.escape) {
52
+ skipCleanup();
53
+ }
54
+ }
55
+ else if (key.escape &&
56
+ (step === "error" || step === "cleanup-complete")) {
57
+ exit();
58
+ }
59
+ });
60
+ // Resume after DNS wait (manual DNS flow)
61
+ const handleDnsComplete = useCallback(async () => {
62
+ if (!config)
63
+ return;
64
+ try {
65
+ setStep("helm-upgrade-tls");
66
+ setStatus((s) => ({
67
+ ...s,
68
+ dnsConfig: "success",
69
+ helmUpgradeTls: "running",
70
+ }));
71
+ // Update helm values to enable TLS
72
+ await updateHelmValuesForTLS(name, true);
73
+ const namespace = getNamespace(config.name);
74
+ const releaseName = getReleaseName(config.name);
75
+ // Upgrade the chart with TLS enabled
76
+ await upgradeChart(name, { releaseName, namespace, version, wait: true });
77
+ setStatus((s) => ({ ...s, helmUpgradeTls: "success" }));
78
+ // Update state
79
+ await updateDeploymentStatus(name, "running", {
80
+ application: {
81
+ appVersion: config.appVersion || "latest",
82
+ hpsVersion: config.hpsVersion || config.appVersion || "latest",
83
+ chartVersion: version || "latest",
84
+ namespace,
85
+ url: `https://${config.domain}`,
86
+ },
87
+ });
88
+ setStep("complete");
89
+ setTimeout(() => exit(), 5000);
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : "TLS upgrade failed";
93
+ setError(message);
94
+ setStep("error");
95
+ setStatus((s) => ({ ...s, helmUpgradeTls: "error" }));
96
+ await updateDeploymentStatus(name, "failed");
97
+ }
98
+ }, [config, name, version, exit]);
99
+ // Skip DNS validation (manual DNS flow)
100
+ const handleDnsSkip = useCallback(async () => {
101
+ if (!config)
102
+ return;
103
+ setStatus((s) => ({
104
+ ...s,
105
+ dnsConfig: "skipped",
106
+ helmUpgradeTls: "skipped",
107
+ }));
108
+ const namespace = getNamespace(config.name);
109
+ // Mark as running without TLS upgrade
110
+ await updateDeploymentStatus(name, "running", {
111
+ application: {
112
+ appVersion: config.appVersion || "latest",
113
+ hpsVersion: config.hpsVersion || config.appVersion || "latest",
114
+ chartVersion: version || "latest",
115
+ namespace,
116
+ url: `https://${config.domain}`,
117
+ },
118
+ });
119
+ setStep("complete");
120
+ setTimeout(() => exit(), 5000);
121
+ }, [config, name, version, exit]);
122
+ useEffect(() => {
123
+ runDeployment();
124
+ }, []);
125
+ async function runDeployment() {
126
+ try {
127
+ // Load configuration
128
+ const cfg = await loadDeploymentConfig(name);
129
+ setConfig(cfg);
130
+ // Determine if External DNS is enabled
131
+ // External DNS = supported provider + auto-manage enabled
132
+ const externalDnsEnabled = cfg.dns.autoManage && isSupportedDnsProvider(cfg.dns.provider);
133
+ setUseExternalDns(externalDnsEnabled);
134
+ // Initialize deployment state
135
+ const existingState = await loadDeploymentState(name);
136
+ const state = existingState || {
137
+ name,
138
+ version: version || "latest",
139
+ createdAt: new Date().toISOString(),
140
+ updatedAt: new Date().toISOString(),
141
+ status: "deploying",
142
+ };
143
+ await saveDeploymentState(name, { ...state, status: "deploying" });
144
+ // Preflight checks
145
+ setStep("preflight");
146
+ setStatus((s) => ({ ...s, preflight: "running" }));
147
+ await runPreflightChecks(cfg);
148
+ setStatus((s) => ({ ...s, preflight: "success" }));
149
+ // Infrastructure provisioning
150
+ const needsInfra = cfg.infrastructure.mode === "provision" && !skipInfra;
151
+ if (needsInfra) {
152
+ setStatus((s) => ({ ...s, infrastructure: "running" }));
153
+ infraStartedRef.current = true; // Mark that we're doing infrastructure work
154
+ // Check if already provisioned
155
+ const hasState = await hasTerraformState(name);
156
+ if (!hasState) {
157
+ setStep("infra-setup");
158
+ await setupTerraformWorkspace(name, cfg.infrastructure.provider);
159
+ }
160
+ setStep("infra-init");
161
+ await terraformInit(name);
162
+ setStep("infra-plan");
163
+ await terraformPlan(name);
164
+ setStep("infra-apply");
165
+ await terraformApply(name);
166
+ setStatus((s) => ({ ...s, infrastructure: "success" }));
167
+ // Update kubeconfig
168
+ setStep("kubeconfig");
169
+ setStatus((s) => ({ ...s, kubeconfig: "running" }));
170
+ await updateKubeconfig(cfg.infrastructure.provider, cfg.infrastructure.clusterName || `${name}-cluster`, cfg.infrastructure.region, {
171
+ gcpProjectId: cfg.infrastructure.gcpProjectId,
172
+ azureResourceGroup: cfg.infrastructure.azureResourceGroup,
173
+ });
174
+ // Note: StorageClass is managed by the Helm chart, not the CLI
175
+ // This avoids conflicts where kubectl-created resources lack Helm ownership labels
176
+ setStatus((s) => ({ ...s, kubeconfig: "success" }));
177
+ }
178
+ else {
179
+ // For existing infrastructure, infrastructure is always skipped
180
+ // kubeconfig may have been updated during preflight if cluster wasn't accessible
181
+ // (in that case, it's already set to 'success'), otherwise mark as skipped
182
+ setStatus((s) => ({
183
+ ...s,
184
+ infrastructure: "skipped",
185
+ kubeconfig: s.kubeconfig === "success" ? "success" : "skipped",
186
+ }));
187
+ }
188
+ // Helm Chart Installation
189
+ setStep("helm-install");
190
+ setStatus((s) => ({ ...s, helmInstall: "running" }));
191
+ const namespace = getNamespace(cfg.name);
192
+ const releaseName = getReleaseName(cfg.name);
193
+ if (externalDnsEnabled) {
194
+ // SINGLE-PHASE DEPLOYMENT (External DNS)
195
+ // Install with TLS enabled from the start - external-dns handles DNS records
196
+ await generateHelmValues(cfg, { tlsEnabled: true });
197
+ await installChart(name, {
198
+ releaseName,
199
+ namespace,
200
+ version,
201
+ wait: true,
202
+ });
203
+ setStatus((s) => ({
204
+ ...s,
205
+ helmInstall: "success",
206
+ dnsConfig: "skipped", // External DNS handles this
207
+ helmUpgradeTls: "skipped", // TLS enabled from start
208
+ }));
209
+ // Update state to running
210
+ await updateDeploymentStatus(name, "running", {
211
+ application: {
212
+ appVersion: cfg.appVersion || "latest",
213
+ hpsVersion: cfg.hpsVersion || cfg.appVersion || "latest",
214
+ chartVersion: version || "latest",
215
+ namespace,
216
+ url: `https://${cfg.domain}`,
217
+ },
218
+ });
219
+ setStep("complete");
220
+ setTimeout(() => exit(), 5000);
221
+ }
222
+ else {
223
+ // TWO-PHASE DEPLOYMENT (Manual DNS)
224
+ // Phase 1: Install without TLS
225
+ await generateHelmValues(cfg, { tlsEnabled: false });
226
+ await installChart(name, {
227
+ releaseName,
228
+ namespace,
229
+ version,
230
+ wait: true,
231
+ });
232
+ setStatus((s) => ({ ...s, helmInstall: "success" }));
233
+ // If skipping DNS, go straight to complete
234
+ if (skipDns) {
235
+ setStatus((s) => ({
236
+ ...s,
237
+ dnsConfig: "skipped",
238
+ helmUpgradeTls: "skipped",
239
+ }));
240
+ await updateDeploymentStatus(name, "waiting-dns", {
241
+ application: {
242
+ appVersion: cfg.appVersion || "latest",
243
+ hpsVersion: cfg.hpsVersion || cfg.appVersion || "latest",
244
+ chartVersion: version || "latest",
245
+ namespace,
246
+ url: `https://${cfg.domain}`,
247
+ },
248
+ });
249
+ setStep("complete");
250
+ setTimeout(() => exit(), 5000);
251
+ return;
252
+ }
253
+ // Update state to waiting for DNS
254
+ await updateDeploymentStatus(name, "waiting-dns");
255
+ // Phase 2: DNS configuration wait
256
+ setStep("dns-wait");
257
+ setStatus((s) => ({ ...s, dnsConfig: "running" }));
258
+ }
259
+ }
260
+ catch (err) {
261
+ const message = err instanceof Error ? err.message : "Unknown error";
262
+ setError(message);
263
+ await updateDeploymentStatus(name, "failed");
264
+ // If we started infrastructure provisioning but failed, offer cleanup
265
+ if (infraStartedRef.current) {
266
+ setStep("cleanup-prompt");
267
+ }
268
+ else {
269
+ setStep("error");
270
+ }
271
+ }
272
+ }
273
+ async function runPreflightChecks(cfg) {
274
+ // Check required tools
275
+ const [helm, kubectl, terraform] = await Promise.all([
276
+ isHelmInstalled(),
277
+ isKubectlInstalled(),
278
+ isTerraformInstalled(),
279
+ ]);
280
+ if (!helm) {
281
+ throw new Error("Helm is not installed. Please install Helm first.");
282
+ }
283
+ if (!kubectl) {
284
+ throw new Error("kubectl is not installed. Please install kubectl first.");
285
+ }
286
+ if (cfg.infrastructure.mode === "provision" && !terraform) {
287
+ throw new Error("Terraform is not installed. Required for infrastructure provisioning.");
288
+ }
289
+ // Check cluster access if using existing infrastructure
290
+ if (cfg.infrastructure.mode === "existing") {
291
+ let clusterError = await checkClusterAccessible();
292
+ // If cluster not accessible but we have provider details, try updating kubeconfig
293
+ if (clusterError &&
294
+ cfg.infrastructure.provider &&
295
+ cfg.infrastructure.region &&
296
+ cfg.infrastructure.clusterName) {
297
+ try {
298
+ // Show visual feedback for kubeconfig update
299
+ setStep("kubeconfig");
300
+ setStatus((s) => ({
301
+ ...s,
302
+ preflight: "success",
303
+ kubeconfig: "running",
304
+ }));
305
+ await updateKubeconfig(cfg.infrastructure.provider, cfg.infrastructure.clusterName, cfg.infrastructure.region, {
306
+ gcpProjectId: cfg.infrastructure.gcpProjectId,
307
+ azureResourceGroup: cfg.infrastructure.azureResourceGroup,
308
+ });
309
+ // Retry cluster access check
310
+ clusterError = await checkClusterAccessible();
311
+ if (!clusterError) {
312
+ setStatus((s) => ({ ...s, kubeconfig: "success" }));
313
+ }
314
+ }
315
+ catch (kubeconfigError) {
316
+ // Kubeconfig update failed, include both errors
317
+ const kubeconfigMsg = kubeconfigError instanceof Error
318
+ ? kubeconfigError.message
319
+ : "Unknown error";
320
+ throw new Error(`Cannot access Kubernetes cluster and kubeconfig update failed:\n` +
321
+ `Cluster error: ${clusterError}\n` +
322
+ `Kubeconfig update error: ${kubeconfigMsg}`);
323
+ }
324
+ }
325
+ if (clusterError) {
326
+ // Provide helpful message based on whether provider details are missing
327
+ if (!cfg.infrastructure.provider ||
328
+ !cfg.infrastructure.region ||
329
+ !cfg.infrastructure.clusterName) {
330
+ throw new Error(`Cannot access Kubernetes cluster:\n${clusterError}\n\n` +
331
+ `Tip: Re-run 'rulebricks init' and provide your cloud provider, region, and cluster name ` +
332
+ `to enable automatic kubeconfig updates.`);
333
+ }
334
+ throw new Error(`Cannot access Kubernetes cluster:\n${clusterError}`);
335
+ }
336
+ }
337
+ }
338
+ // Cleanup prompt screen
339
+ if (step === "cleanup-prompt") {
340
+ return (_jsx(BorderBox, { title: "Deployment Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Infrastructure provisioning failed" }), _jsx(Text, { color: colors.error, children: error }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.warning, bold: true, children: "Partial infrastructure may have been created." }), _jsx(Text, { color: colors.muted, children: "Would you like to clean up to avoid orphaned resources?" })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, bold: true, children: "[Y]" }), _jsxs(Text, { color: colors.muted, children: [" ", "Yes, destroy partial infrastructure"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "[N]" }), _jsxs(Text, { color: colors.muted, children: [" ", "No, keep for debugging (you can run `rulebricks destroy --cluster` later)"] })] })] }) }));
341
+ }
342
+ // Cleanup running screen
343
+ if (step === "cleanup-running") {
344
+ return (_jsx(BorderBox, { title: "Cleaning Up", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Spinner, { label: "Destroying partial infrastructure..." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "This may take several minutes..." }) })] }) }));
345
+ }
346
+ // Cleanup complete screen
347
+ if (step === "cleanup-complete") {
348
+ return (_jsx(BorderBox, { title: "Cleanup Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [cleanupError ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u26A0 Cleanup encountered issues" }), _jsx(Text, { color: colors.warning, children: cleanupError }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Some resources may remain. Run `rulebricks destroy ", name, " ", "--cluster` to retry."] }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Infrastructure cleaned up successfully" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "All partial resources have been destroyed. You can try deploying again." }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
349
+ }
350
+ // Error screen (non-infra failures or when user skips cleanup)
351
+ if (step === "error") {
352
+ // Format error message, preserving newlines for multi-line errors
353
+ const errorLines = error?.split("\n") || ["Unknown error"];
354
+ return (_jsx(BorderBox, { title: "Deployment Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Error" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: errorLines.map((line, i) => (_jsx(Text, { color: line.startsWith(" •") ? colors.muted : colors.error, children: line }, i))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
355
+ }
356
+ // DNS wait screen (only for manual DNS flow)
357
+ if (step === "dns-wait" && config) {
358
+ return (_jsx(DNSWaitScreen, { domain: config.domain, selfHostedSupabase: config.database.type === "self-hosted", namespace: getNamespace(config.name), onComplete: handleDnsComplete, onSkip: handleDnsSkip }));
359
+ }
360
+ // Complete screen
361
+ if (step === "complete") {
362
+ 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
+ }
365
+ // Progress screen
366
+ const helmInstallLabel = useExternalDns
367
+ ? "Helm chart installation (with TLS)"
368
+ : "Helm chart installation";
369
+ return (_jsx(BorderBox, { title: `Deploying ${name}`, children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(StatusLine, { status: status.preflight, label: "Preflight checks" }), _jsx(StatusLine, { status: status.infrastructure, label: "Infrastructure provisioning", detail: step === "infra-setup"
370
+ ? "Setting up workspace"
371
+ : step === "infra-init"
372
+ ? "Initializing Terraform"
373
+ : step === "infra-plan"
374
+ ? "Planning changes"
375
+ : step === "infra-apply"
376
+ ? "Applying infrastructure"
377
+ : 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
+ function getStepLabel(step, useExternalDns) {
380
+ switch (step) {
381
+ case "loading":
382
+ return "Loading configuration...";
383
+ case "preflight":
384
+ return "Running preflight checks...";
385
+ case "infra-setup":
386
+ return "Setting up Terraform workspace...";
387
+ case "infra-init":
388
+ return "Initializing Terraform...";
389
+ case "infra-plan":
390
+ return "Planning infrastructure changes...";
391
+ case "infra-apply":
392
+ return "Creating infrastructure (may take up to 15 minutes)...";
393
+ case "kubeconfig":
394
+ return "Updating kubeconfig...";
395
+ case "helm-install":
396
+ return useExternalDns
397
+ ? "Installing Helm chart with TLS..."
398
+ : "Installing Helm chart...";
399
+ case "dns-wait":
400
+ return "Waiting for DNS configuration...";
401
+ case "helm-upgrade-tls":
402
+ return "Enabling TLS certificates...";
403
+ default:
404
+ return "Processing...";
405
+ }
406
+ }
407
+ export function DeployCommand(props) {
408
+ return (_jsxs(ThemeProvider, { theme: "deploy", children: [_jsx(Logo, {}), _jsx(DeployCommandInner, { ...props })] }));
409
+ }
@@ -0,0 +1,8 @@
1
+ interface DestroyCommandProps {
2
+ name: string;
3
+ cluster?: boolean;
4
+ config?: boolean;
5
+ force?: boolean;
6
+ }
7
+ export declare function DestroyCommand(props: DestroyCommandProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};