@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
@@ -0,0 +1,222 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from "react";
3
+ import { Box, Text, useApp, useStdout } from "ink";
4
+ import SelectInput from "ink-select-input";
5
+ import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
6
+ import { loadDeploymentState } from "../lib/config.js";
7
+ import { getComponentPods, streamLogs, streamMultiPodLogs, VALID_LOG_COMPONENTS, } from "../lib/kubernetes.js";
8
+ import { getNamespace, getReleaseName } from "../types/index.js";
9
+ const COMPONENTS = [
10
+ { label: "Web Application", value: "app" },
11
+ { label: "Solver Handlers", value: "hps" },
12
+ { label: "Solver Workers", value: "workers" },
13
+ { label: "Kafka", value: "kafka" },
14
+ { label: "Supabase", value: "supabase" },
15
+ { label: "Traefik", value: "traefik" },
16
+ ];
17
+ /**
18
+ * Shortens a pod name for display.
19
+ * E.g., "rulebricks-app-7f8b9c6d5-x2k4m" -> "app-x2k4m"
20
+ */
21
+ function shortenPodName(podName) {
22
+ const parts = podName.split("-");
23
+ if (parts.length >= 3) {
24
+ const suffix = parts[parts.length - 1];
25
+ let componentIndex = 0;
26
+ if (parts[0] === "rulebricks" || parts[0].length > 10) {
27
+ componentIndex = 1;
28
+ }
29
+ const component = parts[componentIndex] || parts[0];
30
+ return `${component}-${suffix}`;
31
+ }
32
+ return podName.length > 20 ? podName.substring(0, 17) + "..." : podName;
33
+ }
34
+ /**
35
+ * Colors for split view column headers
36
+ */
37
+ const COLUMN_COLORS = ["cyan", "yellow", "magenta", "green", "blue"];
38
+ /**
39
+ * Maximum number of columns to display in split view
40
+ */
41
+ const MAX_SPLIT_COLUMNS = 3;
42
+ /**
43
+ * Number of log lines to buffer per pod
44
+ */
45
+ const LOG_BUFFER_SIZE = 50;
46
+ function SplitLogView({ pods, namespace, follow, tail, onCleanup, }) {
47
+ const { colors } = useTheme();
48
+ const { stdout } = useStdout();
49
+ const [logBuffers, setLogBuffers] = useState(() => {
50
+ const initial = {};
51
+ for (const pod of pods.slice(0, MAX_SPLIT_COLUMNS)) {
52
+ initial[pod] = { lines: [] };
53
+ }
54
+ return initial;
55
+ });
56
+ const [terminalHeight, setTerminalHeight] = useState(stdout?.rows || 24);
57
+ const cleanupRef = useRef(null);
58
+ // Calculate available height for log content
59
+ const headerHeight = 4; // Header + pod names + separator + footer
60
+ const contentHeight = Math.max(5, terminalHeight - headerHeight);
61
+ const displayedPods = pods.slice(0, MAX_SPLIT_COLUMNS);
62
+ // Handle terminal resize
63
+ useEffect(() => {
64
+ if (stdout) {
65
+ const handleResize = () => {
66
+ setTerminalHeight(stdout.rows || 24);
67
+ };
68
+ stdout.on("resize", handleResize);
69
+ return () => {
70
+ stdout.off("resize", handleResize);
71
+ };
72
+ }
73
+ }, [stdout]);
74
+ // Start log streaming
75
+ useEffect(() => {
76
+ const cleanup = streamMultiPodLogs(displayedPods, namespace, {
77
+ follow,
78
+ tail: tail || LOG_BUFFER_SIZE,
79
+ timestamps: false,
80
+ onLine: (podName, line, _colorIndex) => {
81
+ setLogBuffers((prev) => {
82
+ const buffer = prev[podName] || { lines: [] };
83
+ const newLines = [...buffer.lines, line];
84
+ // Keep only the last LOG_BUFFER_SIZE lines
85
+ if (newLines.length > LOG_BUFFER_SIZE) {
86
+ newLines.splice(0, newLines.length - LOG_BUFFER_SIZE);
87
+ }
88
+ return {
89
+ ...prev,
90
+ [podName]: { lines: newLines },
91
+ };
92
+ });
93
+ },
94
+ });
95
+ cleanupRef.current = cleanup;
96
+ onCleanup(cleanup);
97
+ return () => {
98
+ if (cleanupRef.current) {
99
+ cleanupRef.current();
100
+ }
101
+ };
102
+ }, [displayedPods.join(","), namespace, follow, tail]);
103
+ // Get terminal width for column sizing
104
+ const terminalWidth = stdout?.columns || 80;
105
+ const columnWidth = Math.floor((terminalWidth - displayedPods.length - 1) / displayedPods.length);
106
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.accent, bold: true, children: ["Split view: ", displayedPods.length, " pods", pods.length > MAX_SPLIT_COLUMNS && (_jsxs(Text, { color: colors.muted, children: [" ", "(", pods.length - MAX_SPLIT_COLUMNS, " more not shown)"] }))] }) }), _jsx(Box, { flexDirection: "row", children: displayedPods.map((pod, index) => (_jsx(Box, { width: columnWidth, marginRight: index < displayedPods.length - 1 ? 1 : 0, children: _jsx(Text, { color: COLUMN_COLORS[index % COLUMN_COLORS.length], bold: true, children: shortenPodName(pod).substring(0, columnWidth - 2) }) }, pod))) }), _jsx(Box, { flexDirection: "row", marginBottom: 1, children: displayedPods.map((pod, index) => (_jsx(Box, { width: columnWidth, marginRight: index < displayedPods.length - 1 ? 1 : 0, children: _jsx(Text, { color: colors.muted, children: "─".repeat(Math.max(1, columnWidth - 1)) }) }, `sep-${pod}`))) }), _jsx(Box, { flexDirection: "row", height: contentHeight, children: displayedPods.map((pod, index) => {
107
+ const buffer = logBuffers[pod] || { lines: [] };
108
+ // Show the most recent lines that fit in the content height
109
+ const visibleLines = buffer.lines.slice(-contentHeight);
110
+ return (_jsxs(Box, { width: columnWidth, flexDirection: "column", marginRight: index < displayedPods.length - 1 ? 1 : 0, overflow: "hidden", children: [visibleLines.map((line, lineIndex) => (_jsx(Text, { wrap: "truncate", children: line.substring(0, columnWidth - 1) }, lineIndex))), visibleLines.length === 0 && (_jsx(Text, { color: colors.muted, dimColor: true, children: "Waiting for logs..." }))] }, `log-${pod}`));
111
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Press Ctrl+C to stop" }) })] }));
112
+ }
113
+ function LogsCommandInner({ name, component, follow, tail, split, }) {
114
+ const { exit } = useApp();
115
+ const { colors } = useTheme();
116
+ const [step, setStep] = useState(component && VALID_LOG_COMPONENTS.includes(component)
117
+ ? "loading"
118
+ : "select");
119
+ const [selectedComponent, setSelectedComponent] = useState(component);
120
+ const [pods, setPods] = useState([]);
121
+ const [namespace, setNamespace] = useState("");
122
+ const [error, setError] = useState(null);
123
+ const cleanupRef = useRef(null);
124
+ // Cleanup on unmount
125
+ useEffect(() => {
126
+ return () => {
127
+ if (cleanupRef.current) {
128
+ cleanupRef.current();
129
+ }
130
+ };
131
+ }, []);
132
+ useEffect(() => {
133
+ if (step === "loading") {
134
+ loadPods();
135
+ }
136
+ }, [step, selectedComponent]);
137
+ async function loadPods() {
138
+ try {
139
+ const state = await loadDeploymentState(name);
140
+ // Use namespace from state if available (backwards compat), otherwise compute from deployment name
141
+ const ns = state?.application?.namespace || getNamespace(name);
142
+ setNamespace(ns);
143
+ const releaseName = getReleaseName(name);
144
+ if (!VALID_LOG_COMPONENTS.includes(selectedComponent)) {
145
+ setError(`Unknown component: ${selectedComponent}`);
146
+ setStep("error");
147
+ return;
148
+ }
149
+ const podNames = await getComponentPods(selectedComponent, releaseName, ns);
150
+ if (podNames.length === 0) {
151
+ setError(`No pods found for component: ${selectedComponent}`);
152
+ setStep("error");
153
+ return;
154
+ }
155
+ setPods(podNames);
156
+ const isFollowing = follow ?? true;
157
+ // Use split view if requested and multiple pods exist
158
+ if (split && podNames.length > 1) {
159
+ setStep("streaming-split");
160
+ // Split view will be rendered by the component
161
+ return;
162
+ }
163
+ // For multiple pods without split, use unified multi-pod streaming
164
+ if (podNames.length > 1) {
165
+ setStep("streaming");
166
+ // Start multi-pod log streaming with prefixed output
167
+ cleanupRef.current = streamMultiPodLogs(podNames, ns, {
168
+ follow: isFollowing,
169
+ tail,
170
+ timestamps: true,
171
+ });
172
+ // If not following, wait a bit then exit
173
+ if (!isFollowing) {
174
+ setTimeout(() => {
175
+ if (cleanupRef.current) {
176
+ cleanupRef.current();
177
+ }
178
+ exit();
179
+ }, 2000);
180
+ }
181
+ return;
182
+ }
183
+ // Single pod - use original behavior
184
+ setStep("streaming");
185
+ await streamLogs(podNames[0], ns, { follow: isFollowing, tail });
186
+ // If not following, exit after logs are printed
187
+ if (!isFollowing) {
188
+ exit();
189
+ }
190
+ }
191
+ catch (err) {
192
+ setError(err instanceof Error ? err.message : "Failed to get logs");
193
+ setStep("error");
194
+ }
195
+ }
196
+ const handleComponentSelect = (item) => {
197
+ setSelectedComponent(item.value);
198
+ setStep("loading");
199
+ };
200
+ if (step === "error") {
201
+ return (_jsx(BorderBox, { title: "Logs Error", children: _jsx(Box, { marginY: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", error] }) }) }));
202
+ }
203
+ if (step === "loading") {
204
+ return (_jsx(BorderBox, { title: `Logs: ${selectedComponent}`, children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: `Finding ${selectedComponent} pods...` }) }) }));
205
+ }
206
+ if (step === "streaming-split") {
207
+ const isFollowing = follow ?? true;
208
+ return (_jsx(SplitLogView, { pods: pods, namespace: namespace, follow: isFollowing, tail: tail, onCleanup: (cleanup) => {
209
+ cleanupRef.current = cleanup;
210
+ } }));
211
+ }
212
+ if (step === "streaming") {
213
+ const isFollowing = follow ?? true;
214
+ const podCountText = pods.length > 1 ? `${pods.length} pods` : pods[0];
215
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.accent, bold: true, children: [isFollowing ? "Streaming" : "Showing", " logs from ", podCountText] }), pods.length > 1 && (_jsxs(Text, { color: colors.muted, children: ["Pods: ", pods.map((p, i) => shortenPodName(p)).join(", ")] })), isFollowing && _jsx(Text, { color: colors.muted, children: "Press Ctrl+C to stop" })] }));
216
+ }
217
+ // Component selection
218
+ return (_jsx(BorderBox, { title: "Select Component", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Which component's logs would you like to view?" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: COMPONENTS, onSelect: handleComponentSelect, itemComponent: ({ isSelected, label }) => (_jsx(Text, { color: isSelected ? colors.accent : undefined, children: label })) }) })] }) }));
219
+ }
220
+ export function LogsCommand(props) {
221
+ return (_jsxs(ThemeProvider, { theme: "logs", children: [_jsx(Logo, {}), _jsx(LogsCommandInner, { ...props })] }));
222
+ }
@@ -0,0 +1,7 @@
1
+ type OpenTarget = "all" | "config" | "values" | "terraform";
2
+ interface OpenCommandProps {
3
+ name: string;
4
+ target: OpenTarget;
5
+ }
6
+ export declare function OpenCommand(props: OpenCommandProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,139 @@
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 { spawn } from "child_process";
5
+ import path from "path";
6
+ import { promises as fs } from "fs";
7
+ import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
8
+ import { deploymentExists, getDeploymentDir, getTerraformDir, getHelmValuesPath, } from "../lib/config.js";
9
+ /**
10
+ * Resolves the appropriate command to open files/directories based on OS and environment
11
+ */
12
+ function getOpenCommand() {
13
+ // Check for $EDITOR environment variable first
14
+ const editor = process.env.EDITOR;
15
+ if (editor) {
16
+ return { cmd: editor, args: [] };
17
+ }
18
+ // Fall back to OS-specific defaults
19
+ const platform = process.platform;
20
+ switch (platform) {
21
+ case "darwin":
22
+ return { cmd: "open", args: [] };
23
+ case "win32":
24
+ return { cmd: "cmd", args: ["/c", "start", '""'] };
25
+ case "linux":
26
+ default:
27
+ return { cmd: "xdg-open", args: [] };
28
+ }
29
+ }
30
+ /**
31
+ * Opens a path using the system's default handler or $EDITOR
32
+ */
33
+ async function openPath(targetPath) {
34
+ return new Promise((resolve, reject) => {
35
+ const { cmd, args } = getOpenCommand();
36
+ const child = spawn(cmd, [...args, targetPath], {
37
+ detached: true,
38
+ stdio: "ignore",
39
+ });
40
+ child.on("error", (err) => {
41
+ reject(err);
42
+ });
43
+ // Unref to allow the parent process to exit independently
44
+ child.unref();
45
+ // Give it a moment to start, then resolve
46
+ setTimeout(resolve, 500);
47
+ });
48
+ }
49
+ function OpenCommandInner({ name, target }) {
50
+ const { exit } = useApp();
51
+ const { colors } = useTheme();
52
+ const [step, setStep] = useState("validating");
53
+ const [error, setError] = useState(null);
54
+ const [openedPath, setOpenedPath] = useState(null);
55
+ useEffect(() => {
56
+ (async () => {
57
+ try {
58
+ // Validate deployment exists
59
+ const exists = await deploymentExists(name);
60
+ if (!exists) {
61
+ setError(`Deployment "${name}" not found`);
62
+ setStep("error");
63
+ return;
64
+ }
65
+ // Determine what path to open
66
+ let targetPath;
67
+ const deployDir = getDeploymentDir(name);
68
+ switch (target) {
69
+ case "config":
70
+ targetPath = path.join(deployDir, "config.yaml");
71
+ break;
72
+ case "values":
73
+ targetPath = getHelmValuesPath(name);
74
+ break;
75
+ case "terraform":
76
+ targetPath = getTerraformDir(name);
77
+ break;
78
+ case "all":
79
+ default:
80
+ targetPath = deployDir;
81
+ break;
82
+ }
83
+ // Verify the target exists
84
+ try {
85
+ await fs.access(targetPath);
86
+ }
87
+ catch {
88
+ if (target === "terraform") {
89
+ setError(`Terraform directory not found. Run "rulebricks deploy ${name}" first to create infrastructure files.`);
90
+ }
91
+ else if (target === "values") {
92
+ setError(`values.yaml not found. Run "rulebricks init" or "rulebricks deploy ${name}" first.`);
93
+ }
94
+ else {
95
+ setError(`Path not found: ${targetPath}`);
96
+ }
97
+ setStep("error");
98
+ return;
99
+ }
100
+ setOpenedPath(targetPath);
101
+ setStep("opening");
102
+ // Open the path
103
+ await openPath(targetPath);
104
+ setStep("complete");
105
+ setTimeout(() => exit(), 2000);
106
+ }
107
+ catch (err) {
108
+ setError(err instanceof Error
109
+ ? err.message
110
+ : "Failed to open deployment files");
111
+ setStep("error");
112
+ }
113
+ })();
114
+ }, [name, target, exit]);
115
+ // Validating screen
116
+ if (step === "validating") {
117
+ return (_jsx(BorderBox, { title: "Open Deployment", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Locating deployment files..." }) }) }));
118
+ }
119
+ // Error screen
120
+ if (step === "error") {
121
+ return (_jsx(BorderBox, { title: "Open 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."] }) })] }) }));
122
+ }
123
+ // Opening screen
124
+ if (step === "opening") {
125
+ return (_jsx(BorderBox, { title: "Open Deployment", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Opening files..." }) }) }));
126
+ }
127
+ // Complete screen
128
+ const targetLabel = target === "all"
129
+ ? "deployment directory"
130
+ : target === "config"
131
+ ? "config.yaml"
132
+ : target === "values"
133
+ ? "values.yaml"
134
+ : "terraform directory";
135
+ return (_jsx(BorderBox, { title: "Opened", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: ["\u2713 Opened ", targetLabel] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Path: ", openedPath] }) }), target === "all" && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Available files:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 config.yaml - Deployment configuration"] }), _jsx(Text, { color: colors.muted, children: " \u2022 values.yaml - Helm chart values" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 terraform/ - Infrastructure files"] })] })), (target === "values" || target === "config") && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "Note: Manual edits may desync from wizard-managed settings" }) }))] }) }));
136
+ }
137
+ export function OpenCommand(props) {
138
+ return (_jsxs(ThemeProvider, { theme: "status", children: [_jsx(Logo, {}), _jsx(OpenCommandInner, { ...props })] }));
139
+ }
@@ -0,0 +1,5 @@
1
+ interface StatusCommandProps {
2
+ name: string;
3
+ }
4
+ export declare function StatusCommand(props: StatusCommandProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,125 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import { BorderBox, Section, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
5
+ import { loadDeploymentConfig, loadDeploymentState } from "../lib/config.js";
6
+ import { getPodStatus, getServiceStatus, getIngressStatus, getCertificateStatus, } from "../lib/kubernetes.js";
7
+ import { getInstalledVersion } from "../lib/helm.js";
8
+ import { getNamespace, getReleaseName, } from "../types/index.js";
9
+ function StatusCommandInner({ name, data, }) {
10
+ const { exit } = useApp();
11
+ const { colors } = useTheme();
12
+ const { config, state, clusterStatus } = data;
13
+ useEffect(() => {
14
+ // Auto-exit after displaying
15
+ const timer = setTimeout(() => exit(), 10000);
16
+ return () => clearTimeout(timer);
17
+ }, [exit]);
18
+ // Determine overall status based on deployment state and pod health
19
+ const getOverallStatus = () => {
20
+ // If no state file exists, deployment was never attempted
21
+ if (!state)
22
+ return "not-deployed";
23
+ // Check the deployment state status
24
+ if (state.status === "failed")
25
+ return "failed";
26
+ if (state.status === "destroyed")
27
+ return "destroyed";
28
+ if (state.status === "pending")
29
+ return "pending";
30
+ if (state.status === "deploying")
31
+ return "deploying";
32
+ if (state.status === "waiting-dns")
33
+ return "waiting-dns";
34
+ // If state says running, verify with actual pod status
35
+ const pods = clusterStatus.pods || [];
36
+ if (pods.length === 0) {
37
+ // No pods means something is wrong (unless still deploying)
38
+ return state.status === "running" ? "degraded" : "unknown";
39
+ }
40
+ // Consider a pod healthy if it's ready OR if it's a completed Job pod
41
+ const allPodsHealthy = pods.every((p) => p.ready || p.status === "Succeeded" || p.status === "Completed");
42
+ return allPodsHealthy ? "healthy" : "degraded";
43
+ };
44
+ const overallStatus = getOverallStatus();
45
+ const statusDisplay = {
46
+ healthy: { icon: "●", label: "Healthy", color: colors.success },
47
+ degraded: { icon: "◐", label: "Degraded", color: colors.warning },
48
+ failed: { icon: "✗", label: "Failed", color: colors.error },
49
+ destroyed: { icon: "○", label: "Destroyed", color: colors.muted },
50
+ pending: { icon: "○", label: "Pending", color: colors.muted },
51
+ deploying: { icon: "◐", label: "Deploying", color: colors.accent },
52
+ "waiting-dns": {
53
+ icon: "◐",
54
+ label: "Waiting for DNS",
55
+ color: colors.warning,
56
+ },
57
+ "not-deployed": { icon: "○", label: "Not Deployed", color: colors.muted },
58
+ unknown: { icon: "?", label: "Unknown", color: colors.muted },
59
+ };
60
+ const status = statusDisplay[overallStatus] || statusDisplay["unknown"];
61
+ return (_jsx(BorderBox, { title: `Status: ${name}`, children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Section, { title: "Overview", children: [_jsxs(Text, { children: ["Status:", " ", _jsxs(Text, { color: status.color, children: [status.icon, " ", status.label] })] }), state && (_jsxs(Text, { children: ["Version:", " ", _jsx(Text, { color: colors.accent, children: clusterStatus.version || "Unknown" })] })), _jsxs(Text, { children: ["URL: ", _jsxs(Text, { color: colors.accent, children: ["https://", config.domain] })] })] }), overallStatus === "not-deployed" && (_jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This configuration has not been deployed yet." }), _jsx(Box, { marginTop: 0, children: _jsxs(Text, { color: colors.muted, children: ["Run: ", _jsxs(Text, { color: colors.accent, children: ["rulebricks deploy ", name] })] }) })] })), state && (_jsxs(_Fragment, { children: [_jsx(Section, { title: "Pods", children: clusterStatus.pods.length === 0 ? (_jsx(Text, { color: colors.muted, children: "No pods found" })) : (clusterStatus.pods.map((pod) => {
62
+ // Consider pod healthy if ready OR if it's a completed Job
63
+ const isHealthy = pod.ready ||
64
+ pod.status === "Succeeded" ||
65
+ pod.status === "Completed";
66
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHealthy ? colors.success : colors.warning, children: isHealthy ? "✓" : "○" }), _jsxs(Text, { children: [" ", truncate(pod.name, 40)] }), _jsxs(Text, { color: colors.muted, children: [" ", pod.status] }), pod.restarts > 0 && (_jsxs(Text, { color: colors.warning, children: [" ", "(", pod.restarts, " restarts)"] }))] }, pod.name));
67
+ })) }), _jsxs(Section, { title: "Services", children: [clusterStatus.services.length === 0 ? (_jsx(Text, { color: colors.muted, children: "No services found" })) : (clusterStatus.services.slice(0, 5).map((svc) => (_jsxs(Box, { children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { children: [" ", truncate(svc.name, 30)] }), _jsxs(Text, { color: colors.muted, children: [" ", svc.type] }), svc.externalIP && (_jsxs(Text, { color: colors.accent, children: [" \u2192 ", svc.externalIP] }))] }, svc.name)))), (clusterStatus.services.length || 0) > 5 && (_jsxs(Text, { color: colors.muted, children: ["... and ", (clusterStatus.services.length || 0) - 5, " more"] }))] }), _jsx(Section, { title: "Ingress", children: clusterStatus.ingresses.length === 0 ? (_jsx(Text, { color: colors.muted, children: "No ingresses found" })) : (clusterStatus.ingresses.map((ing) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: ing.address ? colors.success : colors.warning, children: ing.address ? "✓" : "○" }), _jsxs(Text, { children: [" ", ing.name] })] }), ing.hosts.map((host) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: colors.muted, children: ["\u2192 ", host, " ", ing.tls ? "(TLS)" : ""] }) }, host)))] }, ing.name)))) }), _jsx(Section, { title: "TLS Certificates", children: clusterStatus.certificates.length === 0 ? (_jsx(Text, { color: colors.muted, children: "No certificates found" })) : (clusterStatus.certificates.map((cert) => (_jsxs(Box, { children: [_jsx(Text, { color: cert.ready ? colors.success : colors.warning, children: cert.ready ? "✓" : "○" }), _jsxs(Text, { children: [" ", cert.name] }), _jsx(Text, { color: cert.ready ? colors.success : colors.warning, children: cert.ready ? " Ready" : " Pending" })] }, cert.name)))) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Press Ctrl+C to exit" }) })] }) }));
68
+ }
69
+ /**
70
+ * Loader component that fetches data and determines the appropriate theme
71
+ */
72
+ function StatusLoader({ name }) {
73
+ const [loading, setLoading] = useState(true);
74
+ const [data, setData] = useState(null);
75
+ const [error, setError] = useState(null);
76
+ const [theme, setTheme] = useState("status");
77
+ useEffect(() => {
78
+ loadStatus();
79
+ }, []);
80
+ async function loadStatus() {
81
+ try {
82
+ const config = await loadDeploymentConfig(name);
83
+ const state = await loadDeploymentState(name);
84
+ // Determine theme based on whether deployment was attempted
85
+ // Use 'logs' theme (gray/muted) for undeployed, 'status' (green) for deployed
86
+ const selectedTheme = state ? "status" : "logs";
87
+ setTheme(selectedTheme);
88
+ // Use namespace from state if available (backwards compat), otherwise compute from deployment name
89
+ const namespace = state?.application?.namespace || getNamespace(name);
90
+ const releaseName = getReleaseName(name);
91
+ const [pods, services, ingresses, certificates, version] = await Promise.all([
92
+ getPodStatus(namespace),
93
+ getServiceStatus(namespace),
94
+ getIngressStatus(namespace),
95
+ getCertificateStatus(namespace),
96
+ getInstalledVersion(releaseName, namespace),
97
+ ]);
98
+ setData({
99
+ config,
100
+ state,
101
+ clusterStatus: { pods, services, ingresses, certificates, version },
102
+ });
103
+ setLoading(false);
104
+ }
105
+ catch (err) {
106
+ setError(err instanceof Error ? err.message : "Failed to load status");
107
+ setLoading(false);
108
+ }
109
+ }
110
+ if (loading) {
111
+ return (_jsxs(ThemeProvider, { theme: "logs", children: [_jsx(Logo, {}), _jsx(BorderBox, { title: `Status: ${name}`, children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: "Loading deployment status..." }) }) })] }));
112
+ }
113
+ if (error || !data) {
114
+ return (_jsxs(ThemeProvider, { theme: "logs", children: [_jsx(Logo, {}), _jsx(BorderBox, { title: "Status Error", children: _jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error || "Failed to load deployment data"] }) }) })] }));
115
+ }
116
+ return (_jsxs(ThemeProvider, { theme: theme, children: [_jsx(Logo, {}), _jsx(StatusCommandInner, { name: name, data: data })] }));
117
+ }
118
+ export function StatusCommand(props) {
119
+ return _jsx(StatusLoader, { ...props });
120
+ }
121
+ function truncate(str, len) {
122
+ if (str.length <= len)
123
+ return str;
124
+ return str.substring(0, len - 3) + "...";
125
+ }
@@ -0,0 +1,7 @@
1
+ interface UpgradeCommandProps {
2
+ name: string;
3
+ targetVersion?: string;
4
+ dryRun?: boolean;
5
+ }
6
+ export declare function UpgradeCommand(props: UpgradeCommandProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};