@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,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,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,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
|
+
}
|