onflyt-cli 1.0.1-beta.0 → 1.0.1-beta.2
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.
Potentially problematic release.
This version of onflyt-cli might be problematic. Click here for more details.
- package/dist/index.js +72 -368
- package/onflyt.json +10 -0
- package/package.json +4 -5
- package/src/App.tsx +13 -0
- package/src/commands/credits.tsx +151 -0
- package/src/commands/delete.tsx +315 -0
- package/src/commands/deploy.tsx +1039 -0
- package/src/commands/deployments.tsx +331 -0
- package/src/commands/help.tsx +79 -0
- package/src/commands/init.tsx +587 -0
- package/src/commands/login.tsx +207 -0
- package/src/commands/logout.tsx +31 -0
- package/src/commands/logs.tsx +447 -0
- package/src/commands/projects.tsx +287 -0
- package/src/commands/rollback.tsx +455 -0
- package/src/commands/teams.tsx +113 -0
- package/src/commands/whoami.tsx +48 -0
- package/src/components/Loading.tsx +74 -0
- package/src/index.tsx +130 -0
- package/src/lib/api.ts +152 -0
- package/src/lib/config.ts +90 -0
- package/src/lib/deploy-api.ts +511 -0
- package/src/lib/deploy.ts +260 -0
- package/src/lib/framework.ts +227 -0
- package/src/lib/git.ts +179 -0
- package/src/lib/scaffold.ts +225 -0
- package/src/types.d.ts +5 -0
- package/tsconfig.json +17 -0
- package/README.md +0 -338
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import { API_URL, getConfig, saveConfig } from "../lib/config.js";
|
|
6
|
+
import { Logo } from "../components/Loading.js";
|
|
7
|
+
|
|
8
|
+
interface LoginProps {
|
|
9
|
+
openBrowser?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LoadingSpinner = () => (
|
|
13
|
+
<Box>
|
|
14
|
+
<Spinner type="dots" />
|
|
15
|
+
<Text> Starting login...</Text>
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const AlreadyLoggedIn: React.FC<{ user: { name: string; email?: string } }> = ({
|
|
20
|
+
user,
|
|
21
|
+
}) => (
|
|
22
|
+
<Box flexDirection="column" padding={1}>
|
|
23
|
+
<Text bold color="green">
|
|
24
|
+
✓ Welcome back, {user.name}!
|
|
25
|
+
</Text>
|
|
26
|
+
<Box marginTop={1}>
|
|
27
|
+
<Text dimColor>Email: {user.email}</Text>
|
|
28
|
+
</Box>
|
|
29
|
+
<Box marginTop={1}>
|
|
30
|
+
<Text dimColor>Run "onflyt logout" to sign out</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const LoginSuccess: React.FC<{ user: { name: string } }> = ({ user }) => (
|
|
36
|
+
<Box flexDirection="column" padding={1}>
|
|
37
|
+
<Text bold color="green">
|
|
38
|
+
✓ Logged in successfully!
|
|
39
|
+
</Text>
|
|
40
|
+
<Box marginTop={1}>
|
|
41
|
+
<Text>Welcome, {user.name}!</Text>
|
|
42
|
+
</Box>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const LoginError: React.FC<{ error: string }> = ({ error }) => (
|
|
47
|
+
<Box flexDirection="column" padding={1}>
|
|
48
|
+
<Text bold color="red">
|
|
49
|
+
✖ Login failed
|
|
50
|
+
</Text>
|
|
51
|
+
<Text color="red">{error}</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
interface DeviceData {
|
|
56
|
+
user_code: string;
|
|
57
|
+
verification_uri: string;
|
|
58
|
+
device_code: string;
|
|
59
|
+
interval: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Login: React.FC<LoginProps> = ({ openBrowser = true }) => {
|
|
63
|
+
const [deviceData, setDeviceData] = React.useState<DeviceData | null>(null);
|
|
64
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
65
|
+
const [successUser, setSuccessUser] = React.useState<{
|
|
66
|
+
name: string;
|
|
67
|
+
email?: string;
|
|
68
|
+
} | null>(null);
|
|
69
|
+
const [checkedLogin, setCheckedLogin] = React.useState(false);
|
|
70
|
+
const [wasAlreadyLoggedIn, setWasAlreadyLoggedIn] = React.useState(false);
|
|
71
|
+
const browserOpened = React.useRef(false);
|
|
72
|
+
|
|
73
|
+
// Check if already logged in
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
if (config.token && config.user) {
|
|
77
|
+
setSuccessUser(config.user);
|
|
78
|
+
setWasAlreadyLoggedIn(true);
|
|
79
|
+
}
|
|
80
|
+
setCheckedLogin(true);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
if (browserOpened.current || !checkedLogin || successUser) return;
|
|
85
|
+
browserOpened.current = true;
|
|
86
|
+
|
|
87
|
+
const startAuth = async () => {
|
|
88
|
+
try {
|
|
89
|
+
// Step 1: Get device code from our API
|
|
90
|
+
const deviceRes = await fetch(`${API_URL}/auth/device/code`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const deviceData = await deviceRes.json();
|
|
96
|
+
|
|
97
|
+
if (!deviceRes.ok || !deviceData.success) {
|
|
98
|
+
setError(deviceData.error || "Failed to get device code");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setDeviceData({
|
|
103
|
+
user_code: deviceData.user_code,
|
|
104
|
+
verification_uri: deviceData.verification_uri,
|
|
105
|
+
device_code: deviceData.device_code,
|
|
106
|
+
interval: deviceData.interval || 5,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Open browser if enabled
|
|
110
|
+
if (openBrowser) {
|
|
111
|
+
try {
|
|
112
|
+
await open(deviceData.verification_uri, { wait: true });
|
|
113
|
+
} catch {
|
|
114
|
+
// Browser might not open in headless environment
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 2: Poll for token
|
|
119
|
+
let currentInterval = (deviceData.interval || 5) * 1000;
|
|
120
|
+
let remainingAttempts = 150;
|
|
121
|
+
|
|
122
|
+
while (remainingAttempts > 0) {
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, currentInterval));
|
|
124
|
+
remainingAttempts--;
|
|
125
|
+
|
|
126
|
+
const tokenRes = await fetch(`${API_URL}/auth/device/token`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ device_code: deviceData.device_code }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const tokenData = await tokenRes.json();
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
tokenData.success &&
|
|
136
|
+
tokenData.status === "authorized" &&
|
|
137
|
+
tokenData.token
|
|
138
|
+
) {
|
|
139
|
+
saveConfig({
|
|
140
|
+
...getConfig(),
|
|
141
|
+
token: tokenData.token,
|
|
142
|
+
user: tokenData.user,
|
|
143
|
+
lastLogin: new Date().toISOString(),
|
|
144
|
+
});
|
|
145
|
+
setSuccessUser(tokenData.user);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!tokenData.success && tokenData.error) {
|
|
150
|
+
if (tokenData.error === "authorization_pending") {
|
|
151
|
+
continue;
|
|
152
|
+
} else if (tokenData.error === "slow_down") {
|
|
153
|
+
currentInterval += 5000;
|
|
154
|
+
continue;
|
|
155
|
+
} else {
|
|
156
|
+
setError(tokenData.error);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setError("Authorization timeout");
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
setError(err.message || "Failed to start login");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
startAuth();
|
|
169
|
+
}, [checkedLogin, successUser]);
|
|
170
|
+
|
|
171
|
+
if (!checkedLogin) return <LoadingSpinner />;
|
|
172
|
+
if (successUser && wasAlreadyLoggedIn)
|
|
173
|
+
return <AlreadyLoggedIn user={successUser} />;
|
|
174
|
+
if (successUser) return <LoginSuccess user={successUser} />;
|
|
175
|
+
if (error) return <LoginError error={error} />;
|
|
176
|
+
if (!deviceData) return <LoadingSpinner />;
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Box flexDirection="column" padding={1}>
|
|
180
|
+
<Logo />
|
|
181
|
+
<Box marginTop={1}>
|
|
182
|
+
<Text bold>Login to Onflyt</Text>
|
|
183
|
+
</Box>
|
|
184
|
+
<Box marginTop={1}>
|
|
185
|
+
<Text>Visit: </Text>
|
|
186
|
+
<Text color="cyan" underline>
|
|
187
|
+
{deviceData.verification_uri}
|
|
188
|
+
</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
<Box marginTop={1}>
|
|
191
|
+
<Text>Enter code: </Text>
|
|
192
|
+
<Text bold color="green">
|
|
193
|
+
{deviceData.user_code}
|
|
194
|
+
</Text>
|
|
195
|
+
</Box>
|
|
196
|
+
<Box marginTop={1}>
|
|
197
|
+
<Spinner type="dots" />
|
|
198
|
+
<Text> Waiting for authorization...</Text>
|
|
199
|
+
</Box>
|
|
200
|
+
<Box marginTop={1}>
|
|
201
|
+
<Text dimColor>Press Ctrl+C to cancel</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export default Login;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import { getConfig, saveConfig } from "../lib/config.js";
|
|
4
|
+
import { Logo, ErrorDisplay, Success } from "../components/Loading.js";
|
|
5
|
+
|
|
6
|
+
const Logout: React.FC = () => {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
|
|
9
|
+
if (!config.token) {
|
|
10
|
+
return <ErrorDisplay message="Not logged in. Nothing to logout from." />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Clear config
|
|
14
|
+
saveConfig({});
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Box flexDirection="column" padding={1}>
|
|
18
|
+
<Logo />
|
|
19
|
+
<Box marginTop={1}>
|
|
20
|
+
<Text bold color="green">
|
|
21
|
+
✓ Logged out successfully
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
<Box marginTop={1}>
|
|
25
|
+
<Text dimColor>Token cleared from ~/.onflyt/config.json</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default Logout;
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Text, Box, useInput } from "ink";
|
|
3
|
+
import { isLoggedIn, getConfig } from "../lib/config.js";
|
|
4
|
+
import { api } from "../lib/api.js";
|
|
5
|
+
import { Logo } from "../components/Loading.js";
|
|
6
|
+
import Spinner from "ink-spinner";
|
|
7
|
+
|
|
8
|
+
interface LogsProps {
|
|
9
|
+
deploymentId?: string;
|
|
10
|
+
live?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Step =
|
|
14
|
+
| "loading"
|
|
15
|
+
| "loading-projects"
|
|
16
|
+
| "loading-deployments"
|
|
17
|
+
| "team"
|
|
18
|
+
| "project"
|
|
19
|
+
| "deploy"
|
|
20
|
+
| "displaying"
|
|
21
|
+
| "streaming"
|
|
22
|
+
| "error"
|
|
23
|
+
| "done";
|
|
24
|
+
|
|
25
|
+
const Logs: React.FC<LogsProps> = ({ deploymentId, live = false }) => {
|
|
26
|
+
const [step, setStep] = useState<Step>("loading");
|
|
27
|
+
|
|
28
|
+
const [teams, setTeams] = useState<any[]>([]);
|
|
29
|
+
const [projects, setProjects] = useState<any[]>([]);
|
|
30
|
+
const [deployments, setDeployments] = useState<any[]>([]);
|
|
31
|
+
|
|
32
|
+
const [selectedTeamIndex, setSelectedTeamIndex] = useState(0);
|
|
33
|
+
const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
|
|
34
|
+
const [selectedDeployIndex, setSelectedDeployIndex] = useState(0);
|
|
35
|
+
|
|
36
|
+
const [targetProject, setTargetProject] = useState<any>(null);
|
|
37
|
+
const [targetDeploymentId, setTargetDeploymentId] = useState<string | null>(
|
|
38
|
+
null,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
42
|
+
const [errorMsg, setErrorMsg] = useState("");
|
|
43
|
+
const [dotCount, setDotCount] = useState(0);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleSigInt = () => process.exit(0);
|
|
47
|
+
process.on("SIGINT", handleSigInt);
|
|
48
|
+
return () => {
|
|
49
|
+
process.off("SIGINT", handleSigInt);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (step === "done") {
|
|
55
|
+
const timer = setTimeout(() => process.exit(0), 500);
|
|
56
|
+
return () => clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}, [step]);
|
|
59
|
+
|
|
60
|
+
useInput((input, key) => {
|
|
61
|
+
if (input === "q" || input === "Q" || (key.ctrl && input === "c")) {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (step === "team") {
|
|
66
|
+
if (key.upArrow) {
|
|
67
|
+
setSelectedTeamIndex((i) => Math.max(0, i - 1));
|
|
68
|
+
} else if (key.downArrow) {
|
|
69
|
+
setSelectedTeamIndex((i) => Math.min(teams.length - 1, i + 1));
|
|
70
|
+
} else if (key.return) {
|
|
71
|
+
setStep("loading-projects");
|
|
72
|
+
loadProjects(teams[selectedTeamIndex].team.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (step === "project") {
|
|
77
|
+
if (key.upArrow) {
|
|
78
|
+
setSelectedProjectIndex((i) => Math.max(0, i - 1));
|
|
79
|
+
} else if (key.downArrow) {
|
|
80
|
+
setSelectedProjectIndex((i) => Math.min(projects.length - 1, i + 1));
|
|
81
|
+
} else if (key.return) {
|
|
82
|
+
setStep("loading-deployments");
|
|
83
|
+
loadDeployments(projects[selectedProjectIndex]);
|
|
84
|
+
} else if (key.escape) {
|
|
85
|
+
setStep("team");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (step === "deploy") {
|
|
90
|
+
if (key.upArrow) {
|
|
91
|
+
setSelectedDeployIndex((i) => Math.max(0, i - 1));
|
|
92
|
+
} else if (key.downArrow) {
|
|
93
|
+
setSelectedDeployIndex((i) => Math.min(deployments.length - 1, i + 1));
|
|
94
|
+
} else if (key.return) {
|
|
95
|
+
const dep = deployments[selectedDeployIndex];
|
|
96
|
+
setTargetDeploymentId(dep.id);
|
|
97
|
+
setTargetProject(targetProject);
|
|
98
|
+
if (live) {
|
|
99
|
+
startLiveStream(dep.id);
|
|
100
|
+
} else {
|
|
101
|
+
fetchStoredLogs(dep.id);
|
|
102
|
+
}
|
|
103
|
+
} else if (key.escape) {
|
|
104
|
+
setStep("project");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (step !== "streaming") return;
|
|
111
|
+
|
|
112
|
+
const interval = setInterval(() => {
|
|
113
|
+
setDotCount((c) => (c + 1) % 4);
|
|
114
|
+
}, 500);
|
|
115
|
+
|
|
116
|
+
return () => clearInterval(interval);
|
|
117
|
+
}, [step]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isLoggedIn()) {
|
|
121
|
+
setErrorMsg("Not logged in. Run 'onflyt login' first.");
|
|
122
|
+
setStep("error");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (deploymentId) {
|
|
127
|
+
setTargetDeploymentId(deploymentId);
|
|
128
|
+
if (live) {
|
|
129
|
+
startLiveStream(deploymentId);
|
|
130
|
+
} else {
|
|
131
|
+
fetchStoredLogs(deploymentId);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
loadTeams();
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const loadTeams = async () => {
|
|
140
|
+
try {
|
|
141
|
+
const config = getConfig();
|
|
142
|
+
api.setToken(config.token!);
|
|
143
|
+
const meData = await api.get<any>("/auth/me");
|
|
144
|
+
const userTeams = meData.teams || [];
|
|
145
|
+
|
|
146
|
+
if (userTeams.length === 0) {
|
|
147
|
+
setErrorMsg("No teams found");
|
|
148
|
+
setStep("error");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setTeams(userTeams);
|
|
153
|
+
|
|
154
|
+
if (userTeams.length === 1) {
|
|
155
|
+
setSelectedTeamIndex(0);
|
|
156
|
+
setStep("loading-projects");
|
|
157
|
+
loadProjects(userTeams[0].team.id);
|
|
158
|
+
} else {
|
|
159
|
+
setStep("team");
|
|
160
|
+
}
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
setErrorMsg(err.message);
|
|
163
|
+
setStep("error");
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const loadProjects = async (teamId: string) => {
|
|
168
|
+
try {
|
|
169
|
+
const projectsRes = await api.get<any>(`/projects/team/${teamId}`);
|
|
170
|
+
const teamProjects = projectsRes.projects || [];
|
|
171
|
+
|
|
172
|
+
if (teamProjects.length === 0) {
|
|
173
|
+
setErrorMsg("No projects found in this team");
|
|
174
|
+
setStep("error");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setProjects(teamProjects);
|
|
179
|
+
setSelectedProjectIndex(0);
|
|
180
|
+
setStep("project");
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
setErrorMsg(err.message);
|
|
183
|
+
setStep("error");
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const loadDeployments = async (project: any) => {
|
|
188
|
+
try {
|
|
189
|
+
setTargetProject(project);
|
|
190
|
+
const depsRes = await api.get<any>(`/deployments/${project.id}?limit=50`);
|
|
191
|
+
const allDeployments = depsRes.deployments || [];
|
|
192
|
+
|
|
193
|
+
if (allDeployments.length === 0) {
|
|
194
|
+
setErrorMsg("No deployments found for this project");
|
|
195
|
+
setStep("error");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setDeployments(allDeployments);
|
|
200
|
+
setSelectedDeployIndex(0);
|
|
201
|
+
setStep("deploy");
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
setErrorMsg(err.message);
|
|
204
|
+
setStep("error");
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const startLiveStream = (depId: string) => {
|
|
209
|
+
setStep("streaming");
|
|
210
|
+
setLogs([]);
|
|
211
|
+
|
|
212
|
+
import("../lib/deploy-api.js").then(({ streamLogs }) => {
|
|
213
|
+
const cancel = streamLogs(
|
|
214
|
+
depId,
|
|
215
|
+
(log: string) => {
|
|
216
|
+
setLogs((prev) => [...prev.slice(-500), log]);
|
|
217
|
+
},
|
|
218
|
+
() => {
|
|
219
|
+
console.log("\n\x1b[33m⚠ Live logs stream ended\x1b[0m");
|
|
220
|
+
setStep("done");
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
cancel();
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const fetchStoredLogs = (depId: string) => {
|
|
231
|
+
setStep("loading");
|
|
232
|
+
import("../lib/deploy-api.js").then(({ getStoredLogs }) => {
|
|
233
|
+
getStoredLogs(depId, { limit: 200 })
|
|
234
|
+
.then((result: any) => {
|
|
235
|
+
setLogs(
|
|
236
|
+
result.logs.map(
|
|
237
|
+
(l: any) =>
|
|
238
|
+
`[${new Date(l.timestamp).toISOString()}] [${l.level.toUpperCase()}] ${l.message}`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
setStep("displaying");
|
|
242
|
+
})
|
|
243
|
+
.catch((err: any) => {
|
|
244
|
+
setErrorMsg(err.message || "Failed to fetch logs");
|
|
245
|
+
setStep("error");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
step === "loading" ||
|
|
252
|
+
step === "loading-projects" ||
|
|
253
|
+
step === "loading-deployments"
|
|
254
|
+
) {
|
|
255
|
+
return (
|
|
256
|
+
<Box flexDirection="column" padding={1}>
|
|
257
|
+
<Logo />
|
|
258
|
+
<Box marginTop={1}>
|
|
259
|
+
<Text bold>View Logs</Text>
|
|
260
|
+
</Box>
|
|
261
|
+
<Box marginTop={1}>
|
|
262
|
+
<Text dimColor>
|
|
263
|
+
Loading
|
|
264
|
+
{step === "loading-projects"
|
|
265
|
+
? " projects..."
|
|
266
|
+
: step === "loading-deployments"
|
|
267
|
+
? " deployments..."
|
|
268
|
+
: "..."}
|
|
269
|
+
</Text>
|
|
270
|
+
</Box>
|
|
271
|
+
<Box marginTop={1}>
|
|
272
|
+
<Text>
|
|
273
|
+
<Spinner />
|
|
274
|
+
</Text>
|
|
275
|
+
</Box>
|
|
276
|
+
</Box>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (step === "error") {
|
|
281
|
+
return (
|
|
282
|
+
<Box flexDirection="column">
|
|
283
|
+
<Logo />
|
|
284
|
+
<Box marginTop={1}>
|
|
285
|
+
<Text bold color="red">
|
|
286
|
+
✖ Error
|
|
287
|
+
</Text>
|
|
288
|
+
</Box>
|
|
289
|
+
<Box marginTop={1}>
|
|
290
|
+
<Text color="red">{errorMsg}</Text>
|
|
291
|
+
</Box>
|
|
292
|
+
</Box>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (step === "team") {
|
|
297
|
+
return (
|
|
298
|
+
<Box flexDirection="column" padding={1}>
|
|
299
|
+
<Logo />
|
|
300
|
+
<Box marginTop={1}>
|
|
301
|
+
<Text bold>View Logs</Text>
|
|
302
|
+
</Box>
|
|
303
|
+
<Box marginTop={1}>
|
|
304
|
+
<Text dimColor>
|
|
305
|
+
Step 1/3: Select Team (↑↓ navigate, Enter select)
|
|
306
|
+
</Text>
|
|
307
|
+
</Box>
|
|
308
|
+
|
|
309
|
+
<Box marginTop={1} flexDirection="column">
|
|
310
|
+
{teams.map((t, idx) => (
|
|
311
|
+
<Box key={t.team.id} marginTop={1}>
|
|
312
|
+
<Text color={idx === selectedTeamIndex ? "cyan" : "gray"}>
|
|
313
|
+
{idx === selectedTeamIndex ? "▶ " : " "}
|
|
314
|
+
</Text>
|
|
315
|
+
<Text bold={idx === selectedTeamIndex}>{t.team.name}</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
))}
|
|
318
|
+
</Box>
|
|
319
|
+
</Box>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (step === "project") {
|
|
324
|
+
return (
|
|
325
|
+
<Box flexDirection="column" padding={1}>
|
|
326
|
+
<Logo />
|
|
327
|
+
<Box marginTop={1}>
|
|
328
|
+
<Text bold>View Logs</Text>
|
|
329
|
+
</Box>
|
|
330
|
+
<Box marginTop={1}>
|
|
331
|
+
<Text dimColor>
|
|
332
|
+
Step 2/3: Select Project - {teams[selectedTeamIndex]?.team.name}
|
|
333
|
+
</Text>
|
|
334
|
+
</Box>
|
|
335
|
+
<Box>
|
|
336
|
+
<Text dimColor>(↑↓ navigate, Enter select, Esc go back)</Text>
|
|
337
|
+
</Box>
|
|
338
|
+
|
|
339
|
+
<Box marginTop={1} flexDirection="column">
|
|
340
|
+
{projects.map((p, idx) => (
|
|
341
|
+
<Box key={p.id} marginTop={1}>
|
|
342
|
+
<Text color={idx === selectedProjectIndex ? "cyan" : "gray"}>
|
|
343
|
+
{idx === selectedProjectIndex ? "▶ " : " "}
|
|
344
|
+
</Text>
|
|
345
|
+
<Text bold={idx === selectedProjectIndex}>{p.name}</Text>
|
|
346
|
+
</Box>
|
|
347
|
+
))}
|
|
348
|
+
</Box>
|
|
349
|
+
</Box>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (step === "deploy") {
|
|
354
|
+
const formatDate = (dateStr: string) => {
|
|
355
|
+
const date = new Date(dateStr);
|
|
356
|
+
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<Box flexDirection="column" padding={1}>
|
|
361
|
+
<Logo />
|
|
362
|
+
<Box marginTop={1}>
|
|
363
|
+
<Text bold>View Logs</Text>
|
|
364
|
+
</Box>
|
|
365
|
+
<Box marginTop={1}>
|
|
366
|
+
<Text dimColor>
|
|
367
|
+
Step 3/3: Select Deployment - {targetProject?.name}
|
|
368
|
+
</Text>
|
|
369
|
+
</Box>
|
|
370
|
+
<Box>
|
|
371
|
+
<Text dimColor>(↑↓ navigate, Enter select, Esc go back)</Text>
|
|
372
|
+
</Box>
|
|
373
|
+
|
|
374
|
+
<Box marginTop={1} flexDirection="column">
|
|
375
|
+
{deployments.map((dep, idx) => (
|
|
376
|
+
<Box key={dep.id} marginTop={1} flexDirection="column">
|
|
377
|
+
<Box>
|
|
378
|
+
<Text color={idx === selectedDeployIndex ? "cyan" : "gray"}>
|
|
379
|
+
{idx === selectedDeployIndex ? "▶ " : " "}
|
|
380
|
+
</Text>
|
|
381
|
+
<Text bold={idx === selectedDeployIndex}>
|
|
382
|
+
{dep.commitMessage || "Manual Upload"}
|
|
383
|
+
</Text>
|
|
384
|
+
<Text dimColor> [{dep.status}]</Text>
|
|
385
|
+
</Box>
|
|
386
|
+
<Box marginLeft={2}>
|
|
387
|
+
<Text dimColor>ID: {dep.id}</Text>
|
|
388
|
+
</Box>
|
|
389
|
+
<Box marginLeft={2}>
|
|
390
|
+
<Text dimColor>{formatDate(dep.createdAt)}</Text>
|
|
391
|
+
</Box>
|
|
392
|
+
</Box>
|
|
393
|
+
))}
|
|
394
|
+
</Box>
|
|
395
|
+
</Box>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const dots = ".".repeat(dotCount);
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<Box flexDirection="column" padding={1}>
|
|
403
|
+
<Logo />
|
|
404
|
+
|
|
405
|
+
<Box marginTop={1}>
|
|
406
|
+
<Text bold color="cyan">
|
|
407
|
+
Deployment:
|
|
408
|
+
</Text>
|
|
409
|
+
<Text> {targetDeploymentId}</Text>
|
|
410
|
+
{step === "streaming" && <Text dimColor> {dots}</Text>}
|
|
411
|
+
{step === "streaming" && (
|
|
412
|
+
<Box marginLeft={2}>
|
|
413
|
+
<Text dimColor>(Ctrl+C to exit)</Text>
|
|
414
|
+
</Box>
|
|
415
|
+
)}
|
|
416
|
+
</Box>
|
|
417
|
+
|
|
418
|
+
<Box marginTop={1}>
|
|
419
|
+
<Text dimColor>
|
|
420
|
+
{live
|
|
421
|
+
? "Streaming live logs..."
|
|
422
|
+
: `Showing last ${logs.length} log entries`}
|
|
423
|
+
</Text>
|
|
424
|
+
</Box>
|
|
425
|
+
|
|
426
|
+
<Box
|
|
427
|
+
marginTop={1}
|
|
428
|
+
flexDirection="column"
|
|
429
|
+
borderStyle="round"
|
|
430
|
+
borderDimColor
|
|
431
|
+
paddingX={1}
|
|
432
|
+
>
|
|
433
|
+
{logs.length === 0 ? (
|
|
434
|
+
<Text dimColor>No logs available</Text>
|
|
435
|
+
) : (
|
|
436
|
+
logs.map((log, idx) => (
|
|
437
|
+
<Text key={idx} wrap="wrap">
|
|
438
|
+
{log}
|
|
439
|
+
</Text>
|
|
440
|
+
))
|
|
441
|
+
)}
|
|
442
|
+
</Box>
|
|
443
|
+
</Box>
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export default Logs;
|