@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.
- package/README.md +62 -0
- package/dist/commands/clone.d.ts +6 -0
- package/dist/commands/clone.js +60 -0
- package/dist/commands/deploy.d.ts +8 -0
- package/dist/commands/deploy.js +409 -0
- package/dist/commands/destroy.d.ts +8 -0
- package/dist/commands/destroy.js +298 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +201 -0
- package/dist/commands/logs.d.ts +9 -0
- package/dist/commands/logs.js +222 -0
- package/dist/commands/open.d.ts +7 -0
- package/dist/commands/open.js +139 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +125 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +239 -0
- package/dist/components/DNSWaitScreen.d.ts +9 -0
- package/dist/components/DNSWaitScreen.js +73 -0
- package/dist/components/Wizard/WizardContext.d.ts +176 -0
- package/dist/components/Wizard/WizardContext.js +346 -0
- package/dist/components/Wizard/index.d.ts +2 -0
- package/dist/components/Wizard/index.js +2 -0
- package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
- package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
- package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DomainStep.js +126 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
- package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ReviewStep.js +56 -0
- package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SMTPStep.js +191 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
- package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
- package/dist/components/Wizard/steps/TierStep.js +29 -0
- package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
- package/dist/components/Wizard/steps/VersionStep.js +113 -0
- package/dist/components/Wizard/steps/index.d.ts +12 -0
- package/dist/components/Wizard/steps/index.js +12 -0
- package/dist/components/common/AppShell.d.ts +31 -0
- package/dist/components/common/AppShell.js +31 -0
- package/dist/components/common/Box.d.ts +20 -0
- package/dist/components/common/Box.js +20 -0
- package/dist/components/common/Logo.d.ts +7 -0
- package/dist/components/common/Logo.js +22 -0
- package/dist/components/common/Spinner.d.ts +12 -0
- package/dist/components/common/Spinner.js +28 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/index.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +202 -0
- package/dist/lib/cloudCli.d.ts +156 -0
- package/dist/lib/cloudCli.js +691 -0
- package/dist/lib/config.d.ts +91 -0
- package/dist/lib/config.js +278 -0
- package/dist/lib/dns.d.ts +41 -0
- package/dist/lib/dns.js +235 -0
- package/dist/lib/dockerHub.d.ts +57 -0
- package/dist/lib/dockerHub.js +128 -0
- package/dist/lib/helm.d.ts +53 -0
- package/dist/lib/helm.js +209 -0
- package/dist/lib/helmValues.d.ts +17 -0
- package/dist/lib/helmValues.js +693 -0
- package/dist/lib/kubernetes.d.ts +161 -0
- package/dist/lib/kubernetes.js +755 -0
- package/dist/lib/terraform.d.ts +44 -0
- package/dist/lib/terraform.js +230 -0
- package/dist/lib/theme.d.ts +81 -0
- package/dist/lib/theme.js +115 -0
- package/dist/lib/validation.d.ts +47 -0
- package/dist/lib/validation.js +164 -0
- package/dist/lib/versions.d.ts +69 -0
- package/dist/lib/versions.js +139 -0
- package/dist/types/index.d.ts +718 -0
- package/dist/types/index.js +556 -0
- package/email-templates/email_change.html +325 -0
- package/email-templates/invite.html +383 -0
- package/email-templates/password_change.html +414 -0
- package/email-templates/verify.html +396 -0
- package/package.json +78 -0
- package/terraform/aws/main.tf +327 -0
- package/terraform/azure/main.tf +326 -0
- package/terraform/gcp/main.tf +369 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback } 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 { loadDeploymentConfig, loadDeploymentState, deleteDeployment, deploymentExists, } from "../lib/config.js";
|
|
6
|
+
import { uninstallChart, getInstalledVersion } from "../lib/helm.js";
|
|
7
|
+
import { terraformDestroy, hasTerraformState } from "../lib/terraform.js";
|
|
8
|
+
import { deleteNamespace, deletePVCs, isClusterAccessible, namespaceExists, removeKedaFinalizers, } from "../lib/kubernetes.js";
|
|
9
|
+
import { getNamespace, getReleaseName, } from "../types/index.js";
|
|
10
|
+
function DestroyCommandInner({ name, cluster, config, force, }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const { colors } = useTheme();
|
|
13
|
+
const [step, setStep] = useState("loading");
|
|
14
|
+
const [deploymentConfig, setDeploymentConfig] = useState(null);
|
|
15
|
+
const [state, setState] = useState(null);
|
|
16
|
+
const [scope, setScope] = useState(null);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const [confirmText, setConfirmText] = useState("");
|
|
19
|
+
const [status, setStatus] = useState({
|
|
20
|
+
helm: "pending",
|
|
21
|
+
pvc: "pending",
|
|
22
|
+
namespace: "pending",
|
|
23
|
+
infrastructure: "pending",
|
|
24
|
+
cleanup: "pending",
|
|
25
|
+
});
|
|
26
|
+
// Load config and determine scope on mount
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
// Check if deployment exists
|
|
31
|
+
const exists = await deploymentExists(name);
|
|
32
|
+
if (!exists) {
|
|
33
|
+
setError(`Deployment "${name}" not found`);
|
|
34
|
+
setStep("error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Load config (may throw if corrupted)
|
|
38
|
+
let cfg = null;
|
|
39
|
+
try {
|
|
40
|
+
cfg = await loadDeploymentConfig(name);
|
|
41
|
+
setDeploymentConfig(cfg);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Config might be corrupted or missing, that's OK for destroy
|
|
45
|
+
}
|
|
46
|
+
// Load state
|
|
47
|
+
const st = await loadDeploymentState(name);
|
|
48
|
+
setState(st);
|
|
49
|
+
// Determine what was actually deployed
|
|
50
|
+
const deploymentScope = await determineScope(name, cfg, st);
|
|
51
|
+
setScope(deploymentScope);
|
|
52
|
+
if (force) {
|
|
53
|
+
setStep("destroying");
|
|
54
|
+
runDestroy(cfg, st, deploymentScope);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
setStep("confirm");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
setError(err instanceof Error ? err.message : "Failed to load deployment");
|
|
62
|
+
setStep("error");
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
}, [name, force]);
|
|
66
|
+
useInput((input, key) => {
|
|
67
|
+
if (step === "confirm") {
|
|
68
|
+
if (key.return) {
|
|
69
|
+
if (cluster && scope?.hasInfrastructure) {
|
|
70
|
+
if (confirmText === "destroy-all") {
|
|
71
|
+
setStep("destroying");
|
|
72
|
+
runDestroy(deploymentConfig, state, scope);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
setStep("destroying");
|
|
77
|
+
runDestroy(deploymentConfig, state, scope);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (key.escape) {
|
|
81
|
+
exit();
|
|
82
|
+
}
|
|
83
|
+
else if (key.backspace || key.delete) {
|
|
84
|
+
setConfirmText((t) => t.slice(0, -1));
|
|
85
|
+
}
|
|
86
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
87
|
+
setConfirmText((t) => t + input);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (step === "error") {
|
|
91
|
+
if (key.escape || key.return) {
|
|
92
|
+
exit();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const runDestroy = useCallback(async (cfg, st, deploymentScope) => {
|
|
97
|
+
try {
|
|
98
|
+
// Use namespace from state if available (backwards compat), otherwise compute from deployment name
|
|
99
|
+
const namespace = st?.application?.namespace || getNamespace(name);
|
|
100
|
+
const releaseName = getReleaseName(name);
|
|
101
|
+
// Run cluster cleanup if cluster is accessible
|
|
102
|
+
if (deploymentScope.clusterAccessible) {
|
|
103
|
+
// Step 1: Uninstall Helm release (only if namespace exists - helm data is stored there)
|
|
104
|
+
if (deploymentScope.hasHelmRelease && deploymentScope.hasNamespace) {
|
|
105
|
+
setStatus((s) => ({ ...s, helm: "running" }));
|
|
106
|
+
try {
|
|
107
|
+
await uninstallChart(releaseName, namespace, { wait: false });
|
|
108
|
+
setStatus((s) => ({ ...s, helm: "success" }));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Helm release might already be gone, continue anyway
|
|
112
|
+
setStatus((s) => ({ ...s, helm: "error" }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Skip if no helm release OR namespace is already gone
|
|
117
|
+
setStatus((s) => ({ ...s, helm: "skipped" }));
|
|
118
|
+
}
|
|
119
|
+
// Step 2: Delete all PVCs in the namespace
|
|
120
|
+
if (deploymentScope.hasNamespace) {
|
|
121
|
+
setStatus((s) => ({ ...s, pvc: "running" }));
|
|
122
|
+
try {
|
|
123
|
+
await deletePVCs(namespace);
|
|
124
|
+
setStatus((s) => ({ ...s, pvc: "success" }));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// PVCs might not exist, continue anyway
|
|
128
|
+
setStatus((s) => ({ ...s, pvc: "error" }));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
setStatus((s) => ({ ...s, pvc: "skipped" }));
|
|
133
|
+
}
|
|
134
|
+
// Step 3: Delete namespace
|
|
135
|
+
if (deploymentScope.hasNamespace) {
|
|
136
|
+
setStatus((s) => ({ ...s, namespace: "running" }));
|
|
137
|
+
try {
|
|
138
|
+
// Remove KEDA finalizers first to prevent namespace deletion from hanging
|
|
139
|
+
// KEDA finalizers wait for KEDA controller, but it's being deleted too
|
|
140
|
+
await removeKedaFinalizers(namespace);
|
|
141
|
+
await deleteNamespace(namespace);
|
|
142
|
+
setStatus((s) => ({ ...s, namespace: "success" }));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Namespace might already be gone
|
|
146
|
+
setStatus((s) => ({ ...s, namespace: "error" }));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
setStatus((s) => ({ ...s, namespace: "skipped" }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Cluster not accessible - skip all cluster operations
|
|
155
|
+
setStatus((s) => ({
|
|
156
|
+
...s,
|
|
157
|
+
helm: "skipped",
|
|
158
|
+
pvc: "skipped",
|
|
159
|
+
namespace: "skipped",
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
// Destroy infrastructure if requested and it exists
|
|
163
|
+
if (cluster && deploymentScope.hasInfrastructure) {
|
|
164
|
+
setStatus((s) => ({ ...s, infrastructure: "running" }));
|
|
165
|
+
try {
|
|
166
|
+
await terraformDestroy(name);
|
|
167
|
+
setStatus((s) => ({ ...s, infrastructure: "success" }));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
setStatus((s) => ({ ...s, infrastructure: "error" }));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
setStatus((s) => ({ ...s, infrastructure: "skipped" }));
|
|
175
|
+
}
|
|
176
|
+
// Clean up local files (only if --config flag is passed)
|
|
177
|
+
if (config && deploymentScope.hasLocalFiles) {
|
|
178
|
+
setStatus((s) => ({ ...s, cleanup: "running" }));
|
|
179
|
+
try {
|
|
180
|
+
await deleteDeployment(name);
|
|
181
|
+
setStatus((s) => ({ ...s, cleanup: "success" }));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
setStatus((s) => ({ ...s, cleanup: "error" }));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
setStatus((s) => ({ ...s, cleanup: "skipped" }));
|
|
189
|
+
}
|
|
190
|
+
setStep("complete");
|
|
191
|
+
setTimeout(() => exit(), 3000);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
setError(err instanceof Error ? err.message : "Destruction failed");
|
|
195
|
+
setStep("error");
|
|
196
|
+
}
|
|
197
|
+
}, [name, cluster, exit]);
|
|
198
|
+
// Loading screen
|
|
199
|
+
if (step === "loading") {
|
|
200
|
+
return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Checking deployment state..." }) }) }));
|
|
201
|
+
}
|
|
202
|
+
// Error screen
|
|
203
|
+
if (step === "error") {
|
|
204
|
+
return (_jsx(BorderBox, { title: "Destruction 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: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Enter or Esc to exit" }) })] }) }));
|
|
205
|
+
}
|
|
206
|
+
// Complete screen
|
|
207
|
+
if (step === "complete") {
|
|
208
|
+
const cleanedItems = [];
|
|
209
|
+
if (status.helm === "success")
|
|
210
|
+
cleanedItems.push("Helm release");
|
|
211
|
+
if (status.pvc === "success")
|
|
212
|
+
cleanedItems.push("Persistent volume claims");
|
|
213
|
+
if (status.namespace === "success")
|
|
214
|
+
cleanedItems.push("Kubernetes namespace");
|
|
215
|
+
if (status.infrastructure === "success")
|
|
216
|
+
cleanedItems.push("Cloud infrastructure");
|
|
217
|
+
if (status.cleanup === "success")
|
|
218
|
+
cleanedItems.push("Local configuration files");
|
|
219
|
+
// Check if nothing was cleaned in cluster (no helm, no pvc, no namespace)
|
|
220
|
+
const noClusterCleanup = status.helm === "skipped" &&
|
|
221
|
+
status.pvc === "skipped" &&
|
|
222
|
+
status.namespace === "skipped";
|
|
223
|
+
return (_jsx(BorderBox, { title: "Destruction Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: ["\u2713 Deployment \"", name, "\" has been destroyed"] }), cleanedItems.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Cleaned up:" }), cleanedItems.map((item) => (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 ", item] }, item)))] })), noClusterCleanup && status.cleanup === "success" && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Note: No cluster resources found, only local files were cleaned up." }) })), status.cleanup === "skipped" && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Local configuration files preserved in ~/.rulebricks/deployments/", name, "/"] }) }))] }) }));
|
|
224
|
+
}
|
|
225
|
+
// Destroying screen
|
|
226
|
+
if (step === "destroying") {
|
|
227
|
+
// Show cluster operations if cluster is accessible
|
|
228
|
+
const showClusterOps = scope?.clusterAccessible;
|
|
229
|
+
const showInfra = cluster && scope?.hasInfrastructure;
|
|
230
|
+
return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [showClusterOps && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.helm, label: "Uninstalling Helm release" }), _jsx(StatusLine, { status: status.pvc, label: "Deleting persistent volumes" }), _jsx(StatusLine, { status: status.namespace, label: "Deleting namespace" })] })), showInfra && (_jsx(StatusLine, { status: status.infrastructure, label: "Destroying infrastructure" })), config && (_jsx(StatusLine, { status: status.cleanup, label: "Cleaning up local files" })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "Skipping cluster operations (cluster not accessible)" }) })), _jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Destroying deployment..." }) })] }) }));
|
|
231
|
+
}
|
|
232
|
+
// Confirmation screen
|
|
233
|
+
// Check if there's nothing in the cluster to clean up
|
|
234
|
+
const hasClusterResources = scope?.hasHelmRelease || scope?.hasNamespace;
|
|
235
|
+
const onlyLocalFiles = !hasClusterResources && !scope?.hasInfrastructure;
|
|
236
|
+
const needsInfraConfirm = cluster && scope?.hasInfrastructure;
|
|
237
|
+
const willDeleteConfig = config && scope?.hasLocalFiles;
|
|
238
|
+
// Nothing to do if only local files exist but --config not passed
|
|
239
|
+
if (onlyLocalFiles && !config) {
|
|
240
|
+
return (_jsx(BorderBox, { title: "Nothing to Destroy", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.muted, children: "No cluster resources found to clean up." }), _jsx(Text, { color: colors.muted, children: "Local configuration files will be preserved." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Use ", _jsx(Text, { color: colors.accent, children: "--config" }), " to also remove local files."] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
241
|
+
}
|
|
242
|
+
return (_jsx(BorderBox, { title: "Confirm Destruction", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: onlyLocalFiles && config ? (
|
|
243
|
+
// Only cleaning local files (with --config)
|
|
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
|
+
// 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" }) }))] })) }) }));
|
|
247
|
+
}
|
|
248
|
+
export function DestroyCommand(props) {
|
|
249
|
+
return (_jsxs(ThemeProvider, { theme: "destroy", children: [_jsx(Logo, {}), _jsx(DestroyCommandInner, { ...props })] }));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Determines what actually exists by checking cluster state directly.
|
|
253
|
+
* This ensures cleanup works even if local state is out of sync.
|
|
254
|
+
*/
|
|
255
|
+
async function determineScope(name, config, state) {
|
|
256
|
+
// Check if we have local files (we do, since we loaded the deployment)
|
|
257
|
+
const hasLocalFiles = true;
|
|
258
|
+
// Check if infrastructure was provisioned (from local terraform state)
|
|
259
|
+
const hasInfrastructure = await hasTerraformState(name);
|
|
260
|
+
// Use namespace from state if available (backwards compat), otherwise compute from deployment name
|
|
261
|
+
const namespace = state?.application?.namespace || getNamespace(name);
|
|
262
|
+
const releaseName = getReleaseName(name);
|
|
263
|
+
// Check if cluster is accessible
|
|
264
|
+
let clusterAccessible = false;
|
|
265
|
+
try {
|
|
266
|
+
clusterAccessible = await isClusterAccessible();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
clusterAccessible = false;
|
|
270
|
+
}
|
|
271
|
+
// If cluster is accessible, check what actually exists in the cluster
|
|
272
|
+
let hasHelmRelease = false;
|
|
273
|
+
let hasNamespace = false;
|
|
274
|
+
if (clusterAccessible) {
|
|
275
|
+
// Check if Helm release actually exists in the cluster
|
|
276
|
+
try {
|
|
277
|
+
const installedVersion = await getInstalledVersion(releaseName, namespace);
|
|
278
|
+
hasHelmRelease = installedVersion !== null;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
hasHelmRelease = false;
|
|
282
|
+
}
|
|
283
|
+
// Check if namespace exists
|
|
284
|
+
try {
|
|
285
|
+
hasNamespace = await namespaceExists(namespace);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
hasNamespace = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
hasLocalFiles,
|
|
293
|
+
hasHelmRelease,
|
|
294
|
+
hasNamespace,
|
|
295
|
+
hasInfrastructure,
|
|
296
|
+
clusterAccessible,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ProfileConfig } from "../types/index.js";
|
|
2
|
+
interface InitWizardProps {
|
|
3
|
+
initialName?: string;
|
|
4
|
+
profile?: ProfileConfig | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function InitWizard({ initialName, profile: providedProfile, }: InitWizardProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useApp, useStdout } from "ink";
|
|
4
|
+
import { WizardProvider, useWizard, } from "../components/Wizard/WizardContext.js";
|
|
5
|
+
import { DeploymentModeStep, CloudProviderStep, DomainStep, SMTPStep, DatabaseStep, SupabaseCredentialsStep, TierStep, FeaturesStep, FeatureConfigStep, VersionStep, ReviewStep, } from "../components/Wizard/steps/index.js";
|
|
6
|
+
import { AppShell, ProgressHeader, ThemeProvider, useTheme, Logo, LOGO_LINES, } from "../components/common/index.js";
|
|
7
|
+
import { saveDeploymentConfig, deploymentExists, loadProfile, updateProfile, extractProfileFromConfig, } from "../lib/config.js";
|
|
8
|
+
import { generateHelmValues } from "../lib/helmValues.js";
|
|
9
|
+
const STEP_INFO = {
|
|
10
|
+
mode: { title: "Deployment Mode", description: "Choose how to deploy" },
|
|
11
|
+
cloud: { title: "Cloud Provider", description: "Select your cloud provider" },
|
|
12
|
+
domain: { title: "Domain & Email", description: "Configure your domain" },
|
|
13
|
+
smtp: { title: "Email (SMTP)", description: "Configure email delivery" },
|
|
14
|
+
database: { title: "Database", description: "Choose your database setup" },
|
|
15
|
+
"database-creds": {
|
|
16
|
+
title: "Database Credentials",
|
|
17
|
+
description: "Configure database access",
|
|
18
|
+
},
|
|
19
|
+
tier: {
|
|
20
|
+
title: "Performance Tier",
|
|
21
|
+
description: "Select your deployment size",
|
|
22
|
+
},
|
|
23
|
+
features: {
|
|
24
|
+
title: "Optional Features",
|
|
25
|
+
description: "Enable additional features",
|
|
26
|
+
},
|
|
27
|
+
"feature-config": {
|
|
28
|
+
title: "Feature Settings",
|
|
29
|
+
description: "Configure enabled features",
|
|
30
|
+
},
|
|
31
|
+
version: {
|
|
32
|
+
title: "License & Version",
|
|
33
|
+
description: "Enter license and select version",
|
|
34
|
+
},
|
|
35
|
+
review: { title: "Review & Save", description: "Review your configuration" },
|
|
36
|
+
};
|
|
37
|
+
function WizardStepController({ onSaveComplete }) {
|
|
38
|
+
const { state, dispatch, toConfig } = useWizard();
|
|
39
|
+
const { exit } = useApp();
|
|
40
|
+
const { write } = useStdout();
|
|
41
|
+
const { colors } = useTheme();
|
|
42
|
+
const [currentStep, setCurrentStep] = useState("mode");
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
const [complete, setComplete] = useState(false);
|
|
45
|
+
const [error, setError] = useState(null);
|
|
46
|
+
// Clear terminal when transitioning to completion screen
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (complete) {
|
|
49
|
+
// Clear terminal using ANSI escape codes
|
|
50
|
+
write("\x1B[2J\x1B[0;0H");
|
|
51
|
+
}
|
|
52
|
+
}, [complete, write]);
|
|
53
|
+
// Track pending navigation to handle React's async state updates
|
|
54
|
+
const [pendingNav, setPendingNav] = useState(null);
|
|
55
|
+
// Get list of active steps based on config
|
|
56
|
+
const getActiveSteps = useCallback(() => {
|
|
57
|
+
const steps = ["mode"];
|
|
58
|
+
// Cloud provider step for both provision and existing modes
|
|
59
|
+
if (state.infrastructureMode === "provision" ||
|
|
60
|
+
state.infrastructureMode === "existing") {
|
|
61
|
+
steps.push("cloud");
|
|
62
|
+
}
|
|
63
|
+
steps.push("domain", "smtp", "database");
|
|
64
|
+
// Database credentials only for self-hosted
|
|
65
|
+
if (state.databaseType === "self-hosted") {
|
|
66
|
+
steps.push("database-creds");
|
|
67
|
+
}
|
|
68
|
+
steps.push("tier", "features");
|
|
69
|
+
// Feature config only if AI, SSO, monitoring, external logging, or custom emails enabled
|
|
70
|
+
if (state.aiEnabled ||
|
|
71
|
+
state.ssoEnabled ||
|
|
72
|
+
state.monitoringEnabled ||
|
|
73
|
+
state.loggingSink !== "console" ||
|
|
74
|
+
state.customEmailsEnabled) {
|
|
75
|
+
steps.push("feature-config");
|
|
76
|
+
}
|
|
77
|
+
steps.push("version", "review");
|
|
78
|
+
return steps;
|
|
79
|
+
}, [
|
|
80
|
+
state.infrastructureMode,
|
|
81
|
+
state.databaseType,
|
|
82
|
+
state.aiEnabled,
|
|
83
|
+
state.ssoEnabled,
|
|
84
|
+
state.monitoringEnabled,
|
|
85
|
+
state.loggingSink,
|
|
86
|
+
state.customEmailsEnabled,
|
|
87
|
+
]);
|
|
88
|
+
// Handle navigation after state updates - this ensures getActiveSteps has the latest state
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (pendingNav) {
|
|
91
|
+
const steps = getActiveSteps();
|
|
92
|
+
const currentIndex = steps.indexOf(currentStep);
|
|
93
|
+
if (pendingNav === "next" && currentIndex < steps.length - 1) {
|
|
94
|
+
setCurrentStep(steps[currentIndex + 1]);
|
|
95
|
+
}
|
|
96
|
+
else if (pendingNav === "back" && currentIndex > 0) {
|
|
97
|
+
setCurrentStep(steps[currentIndex - 1]);
|
|
98
|
+
}
|
|
99
|
+
setPendingNav(null);
|
|
100
|
+
}
|
|
101
|
+
}, [pendingNav, currentStep, getActiveSteps]);
|
|
102
|
+
// Request navigation - will be processed after React renders with updated state
|
|
103
|
+
const goNext = useCallback(() => {
|
|
104
|
+
setPendingNav("next");
|
|
105
|
+
}, []);
|
|
106
|
+
const goBack = useCallback(() => {
|
|
107
|
+
setPendingNav("back");
|
|
108
|
+
}, []);
|
|
109
|
+
const handleSave = useCallback(async () => {
|
|
110
|
+
const config = toConfig();
|
|
111
|
+
if (!config) {
|
|
112
|
+
setError("Invalid configuration - please check all required fields");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setSaving(true);
|
|
116
|
+
try {
|
|
117
|
+
if (await deploymentExists(config.name)) {
|
|
118
|
+
setError(`Deployment "${config.name}" already exists. Choose a different name.`);
|
|
119
|
+
setSaving(false);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await saveDeploymentConfig(config);
|
|
123
|
+
await generateHelmValues(config);
|
|
124
|
+
// Save configuration values to profile for future deployments
|
|
125
|
+
const profileData = extractProfileFromConfig(config);
|
|
126
|
+
await updateProfile(profileData);
|
|
127
|
+
setComplete(true);
|
|
128
|
+
onSaveComplete?.();
|
|
129
|
+
setTimeout(() => exit(), 4000);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
setError(err instanceof Error ? err.message : "Failed to save configuration");
|
|
133
|
+
setSaving(false);
|
|
134
|
+
}
|
|
135
|
+
}, [toConfig, exit, onSaveComplete]);
|
|
136
|
+
// Get step progress
|
|
137
|
+
const steps = getActiveSteps();
|
|
138
|
+
const stepNumber = steps.indexOf(currentStep) + 1;
|
|
139
|
+
const totalSteps = steps.length;
|
|
140
|
+
const stepInfo = STEP_INFO[currentStep];
|
|
141
|
+
// Completion screen
|
|
142
|
+
if (complete) {
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 2, children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: colors.accent, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.success, bold: true, children: "\u2713 Configuration saved successfully!" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: ["Deployment name:", " ", _jsx(Text, { color: colors.accent, bold: true, children: state.name })] }), _jsxs(Text, { color: colors.muted, children: ["Configuration stored in ~/.rulebricks/deployments/", state.name, "/"] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: colors.muted, children: ["1. Run", " ", _jsxs(Text, { color: colors.accent, children: ["rulebricks deploy ", state.name] }), " ", "to deploy"] }), _jsx(Text, { color: colors.muted, children: "2. Configure your DNS records when prompted" }), _jsxs(Text, { color: colors.muted, children: ["3. Access Rulebricks at", " ", _jsxs(Text, { color: colors.accent, children: ["https://", state.domain] })] })] })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Exiting in a moment..." }) })] })] }));
|
|
144
|
+
}
|
|
145
|
+
// Saving state - simple, without wrapper
|
|
146
|
+
if (saving) {
|
|
147
|
+
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: _jsx(Text, { color: colors.accent, children: "\u29D7 Saving configuration..." }) }));
|
|
148
|
+
}
|
|
149
|
+
// Error state - simple, without wrapper
|
|
150
|
+
if (error) {
|
|
151
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Error" }), _jsx(Text, { color: colors.error, children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Press Ctrl+C to exit and try again" }) })] }));
|
|
152
|
+
}
|
|
153
|
+
// Render current step
|
|
154
|
+
const renderStep = () => {
|
|
155
|
+
switch (currentStep) {
|
|
156
|
+
case "mode":
|
|
157
|
+
return _jsx(DeploymentModeStep, { onComplete: goNext });
|
|
158
|
+
case "cloud":
|
|
159
|
+
return _jsx(CloudProviderStep, { onComplete: goNext, onBack: goBack });
|
|
160
|
+
case "domain":
|
|
161
|
+
return _jsx(DomainStep, { onComplete: goNext, onBack: goBack });
|
|
162
|
+
case "smtp":
|
|
163
|
+
return _jsx(SMTPStep, { onComplete: goNext, onBack: goBack });
|
|
164
|
+
case "database":
|
|
165
|
+
return _jsx(DatabaseStep, { onComplete: goNext, onBack: goBack });
|
|
166
|
+
case "database-creds":
|
|
167
|
+
return _jsx(SupabaseCredentialsStep, { onComplete: goNext, onBack: goBack });
|
|
168
|
+
case "tier":
|
|
169
|
+
return _jsx(TierStep, { onComplete: goNext, onBack: goBack });
|
|
170
|
+
case "features":
|
|
171
|
+
return _jsx(FeaturesStep, { onComplete: goNext, onBack: goBack });
|
|
172
|
+
case "feature-config":
|
|
173
|
+
return _jsx(FeatureConfigStep, { onComplete: goNext, onBack: goBack });
|
|
174
|
+
case "version":
|
|
175
|
+
return _jsx(VersionStep, { onComplete: goNext, onBack: goBack });
|
|
176
|
+
case "review":
|
|
177
|
+
return _jsx(ReviewStep, { onComplete: handleSave, onBack: goBack });
|
|
178
|
+
default:
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
return (_jsxs(AppShell, { title: "Rulebricks Configuration", children: [_jsx(ProgressHeader, { currentStep: stepNumber, totalSteps: totalSteps, stepTitle: stepInfo?.title || "Complete" }), _jsx(Box, { marginTop: 1, children: renderStep() })] }));
|
|
183
|
+
}
|
|
184
|
+
export function InitWizard({ initialName, profile: providedProfile, }) {
|
|
185
|
+
const [profile, setProfile] = useState(providedProfile ?? null);
|
|
186
|
+
const [profileLoaded, setProfileLoaded] = useState(!!providedProfile);
|
|
187
|
+
// Load profile on mount if not provided
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!providedProfile) {
|
|
190
|
+
loadProfile().then((loaded) => {
|
|
191
|
+
setProfile(loaded);
|
|
192
|
+
setProfileLoaded(true);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}, [providedProfile]);
|
|
196
|
+
// Show loading state while profile is being loaded
|
|
197
|
+
if (!profileLoaded) {
|
|
198
|
+
return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { children: "Loading..." }) })] }));
|
|
199
|
+
}
|
|
200
|
+
return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(WizardProvider, { initialName: initialName, profile: profile, children: _jsx(WizardStepController, {}) })] }));
|
|
201
|
+
}
|