@rulebricks/cli 2.1.7 → 2.3.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.
Files changed (117) hide show
  1. package/README.md +51 -16
  2. package/cluster-setup/aws/README.md +96 -47
  3. package/cluster-setup/aws/check-aws-access.sh +216 -52
  4. package/cluster-setup/aws/parameters.json +13 -0
  5. package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
  6. package/cluster-setup/azure/README.md +103 -55
  7. package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
  8. package/cluster-setup/azure/parameters.json +30 -0
  9. package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
  10. package/cluster-setup/gcp/README.md +51 -34
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
  12. package/dist/commands/backup.d.ts +5 -0
  13. package/dist/commands/backup.js +104 -0
  14. package/dist/commands/deploy.d.ts +3 -1
  15. package/dist/commands/deploy.js +226 -326
  16. package/dist/commands/destroy.d.ts +1 -1
  17. package/dist/commands/destroy.js +73 -123
  18. package/dist/commands/init.d.ts +5 -1
  19. package/dist/commands/init.js +78 -54
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.js +74 -0
  22. package/dist/commands/open.d.ts +1 -1
  23. package/dist/commands/open.js +4 -12
  24. package/dist/commands/redeploy.d.ts +6 -0
  25. package/dist/commands/redeploy.js +310 -0
  26. package/dist/commands/restore.d.ts +5 -0
  27. package/dist/commands/restore.js +338 -0
  28. package/dist/commands/status.js +62 -49
  29. package/dist/commands/upgrade.js +74 -51
  30. package/dist/components/DNSWaitScreen.d.ts +5 -1
  31. package/dist/components/DNSWaitScreen.js +47 -41
  32. package/dist/components/Wizard/WizardContext.d.ts +157 -36
  33. package/dist/components/Wizard/WizardContext.js +872 -160
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
  35. package/dist/components/Wizard/steps/DomainStep.js +5 -24
  36. package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
  38. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
  39. package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
  40. package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
  41. package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
  42. package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
  43. package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
  44. package/dist/components/Wizard/steps/ReviewStep.js +56 -12
  45. package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
  46. package/dist/components/Wizard/steps/StorageStep.js +592 -0
  47. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
  48. package/dist/components/Wizard/steps/VersionStep.js +45 -23
  49. package/dist/components/Wizard/steps/index.d.ts +3 -3
  50. package/dist/components/Wizard/steps/index.js +3 -3
  51. package/dist/components/common/CommandApproval.d.ts +12 -0
  52. package/dist/components/common/CommandApproval.js +91 -0
  53. package/dist/components/common/DeploymentPicker.d.ts +14 -0
  54. package/dist/components/common/DeploymentPicker.js +16 -0
  55. package/dist/components/common/index.d.ts +2 -0
  56. package/dist/components/common/index.js +2 -0
  57. package/dist/index.js +94 -62
  58. package/dist/lib/cloudCli.d.ts +134 -63
  59. package/dist/lib/cloudCli.js +512 -220
  60. package/dist/lib/clusterSetupDefaults.d.ts +30 -0
  61. package/dist/lib/clusterSetupDefaults.js +64 -0
  62. package/dist/lib/commandApproval.d.ts +26 -0
  63. package/dist/lib/commandApproval.js +114 -0
  64. package/dist/lib/config.d.ts +12 -10
  65. package/dist/lib/config.js +91 -33
  66. package/dist/lib/configFixtures.d.ts +5 -0
  67. package/dist/lib/configFixtures.js +513 -0
  68. package/dist/lib/deploymentHealth.d.ts +32 -0
  69. package/dist/lib/deploymentHealth.js +157 -0
  70. package/dist/lib/dns.d.ts +1 -1
  71. package/dist/lib/dns.js +19 -1
  72. package/dist/lib/dns.test.d.ts +1 -0
  73. package/dist/lib/dns.test.js +27 -0
  74. package/dist/lib/dockerHub.d.ts +12 -1
  75. package/dist/lib/dockerHub.js +18 -8
  76. package/dist/lib/helm.d.ts +4 -0
  77. package/dist/lib/helm.js +16 -0
  78. package/dist/lib/helmValues.d.ts +25 -0
  79. package/dist/lib/helmValues.js +1762 -289
  80. package/dist/lib/helmValues.test.d.ts +1 -0
  81. package/dist/lib/helmValues.test.js +966 -0
  82. package/dist/lib/htpasswd.d.ts +1 -0
  83. package/dist/lib/htpasswd.js +15 -0
  84. package/dist/lib/kubernetes.d.ts +124 -17
  85. package/dist/lib/kubernetes.js +576 -145
  86. package/dist/lib/secrets.d.ts +23 -0
  87. package/dist/lib/secrets.js +158 -0
  88. package/dist/lib/validateValues.d.ts +31 -0
  89. package/dist/lib/validateValues.js +253 -0
  90. package/dist/lib/versions.d.ts +82 -11
  91. package/dist/lib/versions.js +131 -31
  92. package/dist/lib/versions.test.d.ts +1 -0
  93. package/dist/lib/versions.test.js +81 -0
  94. package/dist/lib/wizardSteps.d.ts +14 -0
  95. package/dist/lib/wizardSteps.js +23 -0
  96. package/dist/lib/workloadIdentity.d.ts +26 -0
  97. package/dist/lib/workloadIdentity.js +323 -0
  98. package/dist/lib/workloadIdentity.test.d.ts +1 -0
  99. package/dist/lib/workloadIdentity.test.js +57 -0
  100. package/dist/types/index.d.ts +1860 -164
  101. package/dist/types/index.js +518 -295
  102. package/package.json +9 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/cluster-setup/aws/cluster.yaml +0 -33
  105. package/cluster-setup/azure/main.bicep +0 -282
  106. package/cluster-setup/azure/main.parameters.json +0 -21
  107. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  108. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  109. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  110. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  111. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  112. package/dist/components/Wizard/steps/TierStep.js +0 -29
  113. package/dist/lib/terraform.d.ts +0 -66
  114. package/dist/lib/terraform.js +0 -754
  115. package/terraform/aws/main.tf +0 -355
  116. package/terraform/azure/main.tf +0 -371
  117. package/terraform/gcp/main.tf +0 -407
@@ -1,8 +1,8 @@
1
1
  interface DestroyCommandProps {
2
2
  name: string;
3
- cluster?: boolean;
4
3
  config?: boolean;
5
4
  force?: boolean;
5
+ purge?: boolean;
6
6
  }
7
7
  export declare function DestroyCommand(props: DestroyCommandProps): import("react/jsx-runtime").JSX.Element;
8
8
  export {};
@@ -1,58 +1,48 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { useState, useCallback } from "react";
2
+ import React, { useCallback, useState } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
5
- import { loadDeploymentConfig, loadDeploymentState, deleteDeployment, deploymentExists, } from "../lib/config.js";
5
+ import { loadDeploymentConfig, loadDeploymentState, deleteDeployment, deploymentExists, updateDeploymentStatus, } from "../lib/config.js";
6
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, }) {
7
+ import { cleanupKubeSystemLeftovers, cleanupNamespaceAPIServices, deleteNamespace, deletePVCs, deleteRulebricksCRDs, isClusterAccessible, isLastRulebricksDeployment, namespaceExists, removeBlockingFinalizers, } from "../lib/kubernetes.js";
8
+ import { getNamespace, getReleaseName } from "../types/index.js";
9
+ function DestroyCommandInner({ name, config, force, purge, }) {
11
10
  const { exit } = useApp();
12
11
  const { colors } = useTheme();
13
12
  const [step, setStep] = useState("loading");
14
- const [deploymentConfig, setDeploymentConfig] = useState(null);
15
13
  const [state, setState] = useState(null);
16
14
  const [scope, setScope] = useState(null);
17
15
  const [error, setError] = useState(null);
18
- const [confirmText, setConfirmText] = useState("");
19
- const [infraError, setInfraError] = useState(null);
20
16
  const [status, setStatus] = useState({
21
17
  helm: "pending",
22
18
  pvc: "pending",
23
19
  namespace: "pending",
24
- infrastructure: "pending",
20
+ kubeSystem: "pending",
21
+ crds: "pending",
25
22
  cleanup: "pending",
26
23
  });
27
- // Load config and determine scope on mount
28
24
  React.useEffect(() => {
29
25
  (async () => {
30
26
  try {
31
- // Check if deployment exists
32
27
  const exists = await deploymentExists(name);
33
28
  if (!exists) {
34
29
  setError(`Deployment "${name}" not found`);
35
30
  setStep("error");
36
31
  return;
37
32
  }
38
- // Load config (may throw if corrupted)
39
- let cfg = null;
40
33
  try {
41
- cfg = await loadDeploymentConfig(name);
42
- setDeploymentConfig(cfg);
34
+ await loadDeploymentConfig(name);
43
35
  }
44
36
  catch {
45
- // Config might be corrupted or missing, that's OK for destroy
37
+ // Config might be corrupted or missing; cluster cleanup can still use state/name.
46
38
  }
47
- // Load state
48
39
  const st = await loadDeploymentState(name);
49
40
  setState(st);
50
- // Determine what was actually deployed
51
- const deploymentScope = await determineScope(name, cfg, st);
41
+ const deploymentScope = await determineScope(name, st);
52
42
  setScope(deploymentScope);
53
43
  if (force) {
54
44
  setStep("destroying");
55
- runDestroy(cfg, st, deploymentScope);
45
+ runDestroy(st, deploymentScope);
56
46
  }
57
47
  else {
58
48
  setStep("confirm");
@@ -67,41 +57,22 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
67
57
  useInput((input, key) => {
68
58
  if (step === "confirm") {
69
59
  if (key.return) {
70
- if (cluster && scope?.hasInfrastructure) {
71
- if (confirmText === "destroy-all") {
72
- setStep("destroying");
73
- runDestroy(deploymentConfig, state, scope);
74
- }
75
- }
76
- else {
77
- setStep("destroying");
78
- runDestroy(deploymentConfig, state, scope);
79
- }
60
+ setStep("destroying");
61
+ runDestroy(state, scope);
80
62
  }
81
63
  else if (key.escape) {
82
64
  exit();
83
65
  }
84
- else if (key.backspace || key.delete) {
85
- setConfirmText((t) => t.slice(0, -1));
86
- }
87
- else if (input && !key.ctrl && !key.meta) {
88
- setConfirmText((t) => t + input);
89
- }
90
66
  }
91
- else if (step === "error") {
92
- if (key.escape || key.return) {
93
- exit();
94
- }
67
+ else if (step === "error" && (key.escape || key.return)) {
68
+ exit();
95
69
  }
96
70
  });
97
- const runDestroy = useCallback(async (cfg, st, deploymentScope) => {
71
+ const runDestroy = useCallback(async (st, deploymentScope) => {
98
72
  try {
99
- // Use namespace from state if available (backwards compat), otherwise compute from deployment name
100
73
  const namespace = st?.application?.namespace || getNamespace(name);
101
74
  const releaseName = getReleaseName(name);
102
- // Run cluster cleanup if cluster is accessible
103
75
  if (deploymentScope.clusterAccessible) {
104
- // Step 1: Uninstall Helm release (only if namespace exists - helm data is stored there)
105
76
  if (deploymentScope.hasHelmRelease && deploymentScope.hasNamespace) {
106
77
  setStatus((s) => ({ ...s, helm: "running" }));
107
78
  try {
@@ -109,15 +80,12 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
109
80
  setStatus((s) => ({ ...s, helm: "success" }));
110
81
  }
111
82
  catch {
112
- // Helm release might already be gone, continue anyway
113
83
  setStatus((s) => ({ ...s, helm: "error" }));
114
84
  }
115
85
  }
116
86
  else {
117
- // Skip if no helm release OR namespace is already gone
118
87
  setStatus((s) => ({ ...s, helm: "skipped" }));
119
88
  }
120
- // Step 2: Delete all PVCs in the namespace
121
89
  if (deploymentScope.hasNamespace) {
122
90
  setStatus((s) => ({ ...s, pvc: "running" }));
123
91
  try {
@@ -125,64 +93,72 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
125
93
  setStatus((s) => ({ ...s, pvc: "success" }));
126
94
  }
127
95
  catch {
128
- // PVCs might not exist, continue anyway
129
96
  setStatus((s) => ({ ...s, pvc: "error" }));
130
97
  }
131
- }
132
- else {
133
- setStatus((s) => ({ ...s, pvc: "skipped" }));
134
- }
135
- // Step 3: Delete namespace
136
- if (deploymentScope.hasNamespace) {
137
98
  setStatus((s) => ({ ...s, namespace: "running" }));
138
99
  try {
139
- // Remove KEDA finalizers first to prevent namespace deletion from hanging
140
- // KEDA finalizers wait for KEDA controller, but it's being deleted too
141
- await removeKedaFinalizers(namespace);
100
+ // Clear teardown deadlocks BEFORE deleting the namespace:
101
+ // - Custom-resource finalizers (KEDA ScaledObjects, cert-manager
102
+ // ACME Challenges/Orders, Strimzi Kafka) wait on controllers
103
+ // removed with the release, so they're never cleared and wedge
104
+ // the namespace (and the CRD) in Terminating.
105
+ // - Aggregated APIServices backed by this namespace's services
106
+ // (KEDA external.metrics, metrics adapters, etc.) go
107
+ // Unavailable as the namespace tears down and break the
108
+ // namespace controller's discovery, wedging it in Terminating.
109
+ await removeBlockingFinalizers(namespace);
110
+ await cleanupNamespaceAPIServices(namespace);
142
111
  await deleteNamespace(namespace);
143
112
  setStatus((s) => ({ ...s, namespace: "success" }));
144
113
  }
145
114
  catch {
146
- // Namespace might already be gone
147
115
  setStatus((s) => ({ ...s, namespace: "error" }));
148
116
  }
149
117
  }
150
118
  else {
151
- setStatus((s) => ({ ...s, namespace: "skipped" }));
119
+ setStatus((s) => ({ ...s, pvc: "skipped", namespace: "skipped" }));
120
+ }
121
+ // Leftovers `helm uninstall` does NOT remove. The prometheus-operator's
122
+ // kube-system kubelet Service is per-release and operator-created, so
123
+ // always clean it (safe; scoped to this release only).
124
+ setStatus((s) => ({ ...s, kubeSystem: "running" }));
125
+ try {
126
+ await cleanupKubeSystemLeftovers(releaseName);
127
+ setStatus((s) => ({ ...s, kubeSystem: "success" }));
128
+ }
129
+ catch {
130
+ setStatus((s) => ({ ...s, kubeSystem: "error" }));
131
+ }
132
+ // CRDs (cert-manager/keda/strimzi/kube-prometheus-stack) ship in crds/
133
+ // dirs and are never removed by helm. They are cluster-SHARED, so only
134
+ // purge them when this is the last Rulebricks deployment on the cluster
135
+ // (or the operator forces --purge) — otherwise deleting a CRD would
136
+ // cascade-delete other deployments' custom resources.
137
+ const purgeCRDs = purge === true || (await isLastRulebricksDeployment(releaseName));
138
+ if (purgeCRDs) {
139
+ setStatus((s) => ({ ...s, crds: "running" }));
140
+ try {
141
+ await deleteRulebricksCRDs();
142
+ setStatus((s) => ({ ...s, crds: "success" }));
143
+ }
144
+ catch {
145
+ setStatus((s) => ({ ...s, crds: "error" }));
146
+ }
147
+ }
148
+ else {
149
+ setStatus((s) => ({ ...s, crds: "skipped" }));
152
150
  }
153
151
  }
154
152
  else {
155
- // Cluster not accessible - skip all cluster operations
156
153
  setStatus((s) => ({
157
154
  ...s,
158
155
  helm: "skipped",
159
156
  pvc: "skipped",
160
157
  namespace: "skipped",
158
+ kubeSystem: "skipped",
159
+ crds: "skipped",
161
160
  }));
162
161
  }
163
- // Destroy infrastructure if requested and it exists
164
- if (cluster && deploymentScope.hasInfrastructure) {
165
- setStatus((s) => ({ ...s, infrastructure: "running" }));
166
- try {
167
- const cloudContext = cfg?.infrastructure.provider && cfg?.infrastructure.region
168
- ? {
169
- provider: cfg.infrastructure.provider,
170
- clusterName: cfg.infrastructure.clusterName || `${name}-cluster`,
171
- region: cfg.infrastructure.region,
172
- }
173
- : undefined;
174
- await terraformDestroy(name, cloudContext);
175
- setStatus((s) => ({ ...s, infrastructure: "success" }));
176
- }
177
- catch (infraErr) {
178
- setInfraError(infraErr instanceof Error ? infraErr.message : "Infrastructure destroy failed");
179
- setStatus((s) => ({ ...s, infrastructure: "error" }));
180
- }
181
- }
182
- else {
183
- setStatus((s) => ({ ...s, infrastructure: "skipped" }));
184
- }
185
- // Clean up local files (only if --config flag is passed)
186
162
  if (config && deploymentScope.hasLocalFiles) {
187
163
  setStatus((s) => ({ ...s, cleanup: "running" }));
188
164
  try {
@@ -196,6 +172,9 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
196
172
  else {
197
173
  setStatus((s) => ({ ...s, cleanup: "skipped" }));
198
174
  }
175
+ if (!config && deploymentScope.clusterAccessible) {
176
+ await updateDeploymentStatus(name, "destroyed");
177
+ }
199
178
  setStep("complete");
200
179
  setTimeout(() => exit(), 3000);
201
180
  }
@@ -203,16 +182,13 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
203
182
  setError(err instanceof Error ? err.message : "Destruction failed");
204
183
  setStep("error");
205
184
  }
206
- }, [name, cluster, config, exit]);
207
- // Loading screen
185
+ }, [name, config, purge, exit]);
208
186
  if (step === "loading") {
209
187
  return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Checking deployment state..." }) }) }));
210
188
  }
211
- // Error screen
212
189
  if (step === "error") {
213
190
  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" }) })] }) }));
214
191
  }
215
- // Complete screen
216
192
  if (step === "complete") {
217
193
  const cleanedItems = [];
218
194
  if (status.helm === "success")
@@ -221,57 +197,35 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
221
197
  cleanedItems.push("Persistent volume claims");
222
198
  if (status.namespace === "success")
223
199
  cleanedItems.push("Kubernetes namespace");
224
- if (status.infrastructure === "success")
225
- cleanedItems.push("Cloud infrastructure");
200
+ if (status.kubeSystem === "success")
201
+ cleanedItems.push("kube-system leftovers (kubelet service)");
202
+ if (status.crds === "success")
203
+ cleanedItems.push("Shared CRDs");
226
204
  if (status.cleanup === "success")
227
205
  cleanedItems.push("Local configuration files");
228
- // Check if nothing was cleaned in cluster (no helm, no pvc, no namespace)
229
206
  const noClusterCleanup = status.helm === "skipped" &&
230
207
  status.pvc === "skipped" &&
231
208
  status.namespace === "skipped";
232
- const hasInfraFailure = status.infrastructure === "error";
233
- const title = hasInfraFailure ? "Destruction Partially Complete" : "Destruction Complete";
234
- return (_jsx(BorderBox, { title: title, children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [hasInfraFailure ? (_jsxs(Text, { color: colors.warning, bold: true, children: ["\u26A0 Deployment \"", name, "\" was partially destroyed"] })) : (_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)))] })), hasInfraFailure && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Infrastructure destroy failed" }), _jsx(Text, { color: colors.error, children: infraError }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Cloud resources may still exist. Run `rulebricks destroy ", name, " ", "--cluster` to retry."] }) })] })), noClusterCleanup && status.cleanup === "success" && !hasInfraFailure && (_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, "/"] }) }))] }) }));
209
+ 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, "/"] }) }))] }) }));
235
210
  }
236
- // Destroying screen
237
211
  if (step === "destroying") {
238
- // Show cluster operations if cluster is accessible
239
- const showClusterOps = scope?.clusterAccessible;
240
- const showInfra = cluster && scope?.hasInfrastructure;
241
- 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..." }) })] }) }));
212
+ return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [scope?.clusterAccessible && (_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" }), _jsx(StatusLine, { status: status.kubeSystem, label: "Removing kube-system leftovers" }), _jsx(StatusLine, { status: status.crds, label: "Removing shared CRDs" })] })), 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..." }) })] }) }));
242
213
  }
243
- // Confirmation screen
244
- // Check if there's nothing in the cluster to clean up
245
214
  const hasClusterResources = scope?.hasHelmRelease || scope?.hasNamespace;
246
- const onlyLocalFiles = !hasClusterResources && !scope?.hasInfrastructure;
247
- const needsInfraConfirm = cluster && scope?.hasInfrastructure;
215
+ const onlyLocalFiles = !hasClusterResources;
248
216
  const willDeleteConfig = config && scope?.hasLocalFiles;
249
- // Nothing to do if only local files exist but --config not passed
250
217
  if (onlyLocalFiles && !config) {
251
218
  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" }) })] }) }));
252
219
  }
253
- return (_jsx(BorderBox, { title: "Confirm Destruction", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: onlyLocalFiles && config ? (
254
- // Only cleaning local files (with --config)
255
- _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" }) })] })) : (
256
- // Full destruction
257
- _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. Some 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" }) }))] })) }) }));
220
+ return (_jsx(BorderBox, { title: "Confirm Destruction", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [onlyLocalFiles && config ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "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." })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "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" }), _jsx(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" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !willDeleteConfig && (_jsx(Box, { marginTop: 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: "Cluster is not accessible. Some cluster resources may need manual cleanup." }) }))] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) })] }) }));
258
221
  }
259
222
  export function DestroyCommand(props) {
260
223
  return (_jsxs(ThemeProvider, { theme: "destroy", children: [_jsx(Logo, {}), _jsx(DestroyCommandInner, { ...props })] }));
261
224
  }
262
- /**
263
- * Determines what actually exists by checking cluster state directly.
264
- * This ensures cleanup works even if local state is out of sync.
265
- */
266
- async function determineScope(name, config, state) {
267
- // Check if we have local files (we do, since we loaded the deployment)
225
+ async function determineScope(name, state) {
268
226
  const hasLocalFiles = true;
269
- // Check if infrastructure was provisioned (from local terraform state)
270
- const hasInfrastructure = await hasTerraformState(name);
271
- // Use namespace from state if available (backwards compat), otherwise compute from deployment name
272
227
  const namespace = state?.application?.namespace || getNamespace(name);
273
228
  const releaseName = getReleaseName(name);
274
- // Check if cluster is accessible
275
229
  let clusterAccessible = false;
276
230
  try {
277
231
  clusterAccessible = await isClusterAccessible();
@@ -279,11 +233,9 @@ async function determineScope(name, config, state) {
279
233
  catch {
280
234
  clusterAccessible = false;
281
235
  }
282
- // If cluster is accessible, check what actually exists in the cluster
283
236
  let hasHelmRelease = false;
284
237
  let hasNamespace = false;
285
238
  if (clusterAccessible) {
286
- // Check if Helm release actually exists in the cluster
287
239
  try {
288
240
  const installedVersion = await getInstalledVersion(releaseName, namespace);
289
241
  hasHelmRelease = installedVersion !== null;
@@ -291,7 +243,6 @@ async function determineScope(name, config, state) {
291
243
  catch {
292
244
  hasHelmRelease = false;
293
245
  }
294
- // Check if namespace exists
295
246
  try {
296
247
  hasNamespace = await namespaceExists(namespace);
297
248
  }
@@ -303,7 +254,6 @@ async function determineScope(name, config, state) {
303
254
  hasLocalFiles,
304
255
  hasHelmRelease,
305
256
  hasNamespace,
306
- hasInfrastructure,
307
257
  clusterAccessible,
308
258
  };
309
259
  }
@@ -1,7 +1,11 @@
1
+ import { WizardState } from "../components/Wizard/WizardContext.js";
1
2
  import { ProfileConfig } from "../types/index.js";
2
3
  interface InitWizardProps {
3
4
  initialName?: string;
5
+ initialState?: WizardState;
6
+ mode?: "create" | "redeploy";
7
+ onSaveComplete?: () => void;
4
8
  profile?: ProfileConfig | null;
5
9
  }
6
- export declare function InitWizard({ initialName, profile: providedProfile, }: InitWizardProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function InitWizard({ initialName, initialState, mode, onSaveComplete, profile: providedProfile, }: InitWizardProps): import("react/jsx-runtime").JSX.Element;
7
11
  export {};
@@ -2,29 +2,37 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useEffect } from "react";
3
3
  import { Box, Text, useApp, useStdout } from "ink";
4
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
- import { inferClusterTier } from "../lib/kubernetes.js";
5
+ import { CloudProviderStep, DomainStep, SMTPStep, DatabaseStep, SupabaseCredentialsStep, FeaturesStep, StorageStep, ObservabilityStep, ExternalServicesStep, FeatureConfigStep, VersionStep, ReviewStep, } from "../components/Wizard/steps/index.js";
6
+ import { AppShell, ProgressHeader, ThemeProvider, useTheme, Logo, LOGO_LINES, CommandApprovalProvider, } from "../components/common/index.js";
7
+ import { saveDeploymentConfig, deploymentExists, loadHelmValues, loadProfile, saveHelmValues, updateProfile, extractProfileFromConfig, } from "../lib/config.js";
8
+ import { buildHelmValues, generateHelmValues, mergeHelmValues, } from "../lib/helmValues.js";
9
+ import { assertValidHelmValues } from "../lib/validateValues.js";
10
+ import { getActiveWizardSteps, } from "../lib/wizardSteps.js";
10
11
  const STEP_INFO = {
11
- mode: { title: "Deployment Mode", description: "Choose how to deploy" },
12
12
  cloud: { title: "Cloud Provider", description: "Select your cloud provider" },
13
- domain: { title: "Domain & Email", description: "Configure your domain" },
13
+ domain: { title: "Domain & DNS", description: "Configure your domain and DNS" },
14
14
  smtp: { title: "Email (SMTP)", description: "Configure email delivery" },
15
15
  database: { title: "Database", description: "Choose your database setup" },
16
16
  "database-creds": {
17
17
  title: "Database Credentials",
18
18
  description: "Configure database access",
19
19
  },
20
- tier: {
21
- title: "Performance Tier",
22
- description: "Select your deployment size",
20
+ "external-services": {
21
+ title: "External Services",
22
+ description: "Use managed Redis/Kafka (optional)",
23
23
  },
24
24
  features: {
25
25
  title: "Optional Features",
26
26
  description: "Enable additional features",
27
27
  },
28
+ storage: {
29
+ title: "Storage & Backups",
30
+ description: "Configure object storage and database backups",
31
+ },
32
+ observability: {
33
+ title: "Observability",
34
+ description: "Choose built-in ClickStack or export to your own systems",
35
+ },
28
36
  "feature-config": {
29
37
  title: "Feature Settings",
30
38
  description: "Configure enabled features",
@@ -35,12 +43,12 @@ const STEP_INFO = {
35
43
  },
36
44
  review: { title: "Review & Save", description: "Review your configuration" },
37
45
  };
38
- function WizardStepController({ onSaveComplete }) {
39
- const { state, dispatch, toConfig } = useWizard();
46
+ function WizardStepController({ mode, onSaveComplete, }) {
47
+ const { state, dispatch, toConfig, configIssues } = useWizard();
40
48
  const { exit } = useApp();
41
49
  const { write } = useStdout();
42
50
  const { colors } = useTheme();
43
- const [currentStep, setCurrentStep] = useState("mode");
51
+ const [currentStep, setCurrentStep] = useState(mode === "redeploy" ? "domain" : "cloud");
44
52
  const [saving, setSaving] = useState(false);
45
53
  const [complete, setComplete] = useState(false);
46
54
  const [error, setError] = useState(null);
@@ -53,39 +61,22 @@ function WizardStepController({ onSaveComplete }) {
53
61
  }, [complete, write]);
54
62
  // Track pending navigation to handle React's async state updates
55
63
  const [pendingNav, setPendingNav] = useState(null);
64
+ // Direction of the last navigation, so multi-substep steps can resume at their
65
+ // end when the user navigates back into them (e.g. Esc from the Storage step).
66
+ const [navDirection, setNavDirection] = useState("forward");
56
67
  // Get list of active steps based on config
57
68
  const getActiveSteps = useCallback(() => {
58
- const steps = ["mode"];
59
- // Cloud provider step for both provision and existing modes
60
- if (state.infrastructureMode === "provision" ||
61
- state.infrastructureMode === "existing") {
62
- steps.push("cloud");
63
- }
64
- steps.push("domain", "smtp", "database");
65
- // Database credentials only for self-hosted
66
- if (state.databaseType === "self-hosted") {
67
- steps.push("database-creds");
68
- }
69
- if (state.infrastructureMode === "provision") {
70
- steps.push("tier");
71
- }
72
- steps.push("features");
73
- // Feature config only if AI, SSO, monitoring, external logging, or custom emails enabled
74
- if (state.aiEnabled ||
75
- state.ssoEnabled ||
76
- state.monitoringEnabled ||
77
- state.loggingSink !== "console" ||
78
- state.customEmailsEnabled) {
79
- steps.push("feature-config");
80
- }
81
- steps.push("version", "review");
82
- return steps;
69
+ return getActiveWizardSteps(state, mode);
83
70
  }, [
84
- state.infrastructureMode,
71
+ mode,
85
72
  state.databaseType,
86
73
  state.aiEnabled,
87
74
  state.ssoEnabled,
88
- state.monitoringEnabled,
75
+ state.clickStackEnabled,
76
+ state.metricsExportEnabled,
77
+ state.tracingEnabled,
78
+ state.appLogsEnabled,
79
+ state.valkeyAdminEnabled,
89
80
  state.loggingSink,
90
81
  state.customEmailsEnabled,
91
82
  ]);
@@ -95,9 +86,11 @@ function WizardStepController({ onSaveComplete }) {
95
86
  const steps = getActiveSteps();
96
87
  const currentIndex = steps.indexOf(currentStep);
97
88
  if (pendingNav === "next" && currentIndex < steps.length - 1) {
89
+ setNavDirection("forward");
98
90
  setCurrentStep(steps[currentIndex + 1]);
99
91
  }
100
92
  else if (pendingNav === "back" && currentIndex > 0) {
93
+ setNavDirection("back");
101
94
  setCurrentStep(steps[currentIndex - 1]);
102
95
  }
103
96
  setPendingNav(null);
@@ -111,17 +104,36 @@ function WizardStepController({ onSaveComplete }) {
111
104
  setPendingNav("back");
112
105
  }, []);
113
106
  const handleSave = useCallback(async () => {
114
- const inferredTier = state.infrastructureMode === "existing"
115
- ? (await inferClusterTier()) || state.tier || "small"
116
- : state.tier || undefined;
117
- const config = toConfig({ tier: inferredTier });
107
+ const config = toConfig({
108
+ nodeArchitecture: state.nodeArchitecture || undefined,
109
+ arm64TolerationRequired: state.arm64TolerationRequired,
110
+ storageClass: state.storageClass || undefined,
111
+ storageProvisioner: state.storageProvisioner || undefined,
112
+ schedulableNodeCount: state.schedulableNodeCount || undefined,
113
+ totalCpuCores: state.totalCpuCores || undefined,
114
+ totalMemoryGi: state.totalMemoryGi || undefined,
115
+ eligibleCpuCores: state.eligibleCpuCores || undefined,
116
+ eligibleMemoryGi: state.eligibleMemoryGi || undefined,
117
+ totalPersistentStorageGi: state.totalPersistentStorageGi || undefined,
118
+ });
118
119
  if (!config) {
119
- setError("Invalid configuration - please check all required fields");
120
+ const issues = configIssues();
121
+ setError(issues.length > 0
122
+ ? `Configuration is incomplete:\n${issues.map((i) => ` • ${i}`).join("\n")}`
123
+ : "Invalid configuration - please check all required fields");
120
124
  return;
121
125
  }
122
126
  setSaving(true);
123
127
  try {
124
128
  if (await deploymentExists(config.name)) {
129
+ if (mode === "redeploy") {
130
+ await saveRedeployValues(config);
131
+ await saveDeploymentConfig(config);
132
+ const profileData = extractProfileFromConfig(config);
133
+ await updateProfile(profileData);
134
+ onSaveComplete?.();
135
+ return;
136
+ }
125
137
  setError(`Deployment "${config.name}" already exists. Choose a different name.`);
126
138
  setSaving(false);
127
139
  return;
@@ -139,7 +151,17 @@ function WizardStepController({ onSaveComplete }) {
139
151
  setError(err instanceof Error ? err.message : "Failed to save configuration");
140
152
  setSaving(false);
141
153
  }
142
- }, [toConfig, exit, onSaveComplete]);
154
+ }, [toConfig, configIssues, state, exit, onSaveComplete, mode]);
155
+ async function saveRedeployValues(config) {
156
+ if (!config)
157
+ return;
158
+ const existingValues = (await loadHelmValues(config.name)) ?? {};
159
+ const generatedValues = buildHelmValues(config);
160
+ const mergedValues = mergeHelmValues(existingValues, generatedValues);
161
+ // Guardrail: a merge with stale manual edits must still satisfy the chart.
162
+ assertValidHelmValues(mergedValues);
163
+ await saveHelmValues(config.name, mergedValues);
164
+ }
143
165
  // Get step progress
144
166
  const steps = getActiveSteps();
145
167
  const stepNumber = steps.indexOf(currentStep) + 1;
@@ -160,8 +182,6 @@ function WizardStepController({ onSaveComplete }) {
160
182
  // Render current step
161
183
  const renderStep = () => {
162
184
  switch (currentStep) {
163
- case "mode":
164
- return _jsx(DeploymentModeStep, { onComplete: goNext });
165
185
  case "cloud":
166
186
  return _jsx(CloudProviderStep, { onComplete: goNext, onBack: goBack });
167
187
  case "domain":
@@ -172,23 +192,27 @@ function WizardStepController({ onSaveComplete }) {
172
192
  return _jsx(DatabaseStep, { onComplete: goNext, onBack: goBack });
173
193
  case "database-creds":
174
194
  return _jsx(SupabaseCredentialsStep, { onComplete: goNext, onBack: goBack });
175
- case "tier":
176
- return _jsx(TierStep, { onComplete: goNext, onBack: goBack });
195
+ case "external-services":
196
+ return _jsx(ExternalServicesStep, { onComplete: goNext, onBack: goBack });
177
197
  case "features":
178
198
  return _jsx(FeaturesStep, { onComplete: goNext, onBack: goBack });
199
+ case "storage":
200
+ return _jsx(StorageStep, { onComplete: goNext, onBack: goBack });
201
+ case "observability":
202
+ return _jsx(ObservabilityStep, { onComplete: goNext, onBack: goBack });
179
203
  case "feature-config":
180
- return _jsx(FeatureConfigStep, { onComplete: goNext, onBack: goBack });
204
+ return (_jsx(FeatureConfigStep, { onComplete: goNext, onBack: goBack, entryDirection: navDirection }));
181
205
  case "version":
182
206
  return _jsx(VersionStep, { onComplete: goNext, onBack: goBack });
183
207
  case "review":
184
- return _jsx(ReviewStep, { onComplete: handleSave, onBack: goBack });
208
+ return (_jsx(ReviewStep, { onComplete: handleSave, onBack: goBack, allowEditName: mode === "create" }));
185
209
  default:
186
210
  return null;
187
211
  }
188
212
  };
189
213
  return (_jsxs(AppShell, { title: "Rulebricks Configuration", children: [_jsx(ProgressHeader, { currentStep: stepNumber, totalSteps: totalSteps, stepTitle: stepInfo?.title || "Complete" }), _jsx(Box, { marginTop: 1, children: renderStep() })] }));
190
214
  }
191
- export function InitWizard({ initialName, profile: providedProfile, }) {
215
+ export function InitWizard({ initialName, initialState, mode = "create", onSaveComplete, profile: providedProfile, }) {
192
216
  const [profile, setProfile] = useState(providedProfile ?? null);
193
217
  const [profileLoaded, setProfileLoaded] = useState(!!providedProfile);
194
218
  // Load profile on mount if not provided
@@ -204,5 +228,5 @@ export function InitWizard({ initialName, profile: providedProfile, }) {
204
228
  if (!profileLoaded) {
205
229
  return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { children: "Loading..." }) })] }));
206
230
  }
207
- return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(WizardProvider, { initialName: initialName, profile: profile, children: _jsx(WizardStepController, {}) })] }));
231
+ return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(CommandApprovalProvider, { children: _jsx(WizardProvider, { initialName: initialName, initialState: initialState, profile: profile, children: _jsx(WizardStepController, { mode: mode, onSaveComplete: onSaveComplete }) }) })] }));
208
232
  }
@@ -0,0 +1 @@
1
+ export declare function ListCommand(): import("react/jsx-runtime").JSX.Element;