onflyt-cli 0.1.0-beta

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.
@@ -0,0 +1,455 @@
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 RollbackProps {
9
+ deploymentId?: string;
10
+ }
11
+
12
+ const Rollback: React.FC<RollbackProps> = ({ deploymentId }) => {
13
+ const [step, setStep] = useState<
14
+ | "loading"
15
+ | "loading-projects"
16
+ | "loading-deployments"
17
+ | "team"
18
+ | "project"
19
+ | "deploy"
20
+ | "confirm"
21
+ | "rolling"
22
+ | "done"
23
+ | "error"
24
+ >("loading");
25
+
26
+ const [teams, setTeams] = useState<any[]>([]);
27
+ const [projects, setProjects] = useState<any[]>([]);
28
+ const [deployments, setDeployments] = useState<any[]>([]);
29
+
30
+ const [selectedTeamIndex, setSelectedTeamIndex] = useState(0);
31
+ const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
32
+ const [selectedDeployIndex, setSelectedDeployIndex] = useState(0);
33
+
34
+ const [targetProject, setTargetProject] = useState<any>(null);
35
+ const [targetDeployment, setTargetDeployment] = useState<any>(null);
36
+ const [errorMsg, setErrorMsg] = useState("");
37
+
38
+ useEffect(() => {
39
+ const handleSigInt = () => process.exit(0);
40
+ process.on("SIGINT", handleSigInt);
41
+ return () => {
42
+ process.off("SIGINT", handleSigInt);
43
+ };
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ if (step === "done" || step === "error") {
48
+ const timer = setTimeout(() => process.exit(0), 500);
49
+ return () => clearTimeout(timer);
50
+ }
51
+ }, [step]);
52
+
53
+ useInput((input, key) => {
54
+ if (input === "q" || input === "Q" || (key.ctrl && input === "c")) {
55
+ process.exit(0);
56
+ }
57
+
58
+ if (step === "team") {
59
+ if (key.upArrow) {
60
+ setSelectedTeamIndex((i) => Math.max(0, i - 1));
61
+ } else if (key.downArrow) {
62
+ setSelectedTeamIndex((i) => Math.min(teams.length - 1, i + 1));
63
+ } else if (key.return) {
64
+ setStep("loading-projects");
65
+ loadProjects(teams[selectedTeamIndex].team.id);
66
+ }
67
+ }
68
+
69
+ if (step === "project") {
70
+ if (key.upArrow) {
71
+ setSelectedProjectIndex((i) => Math.max(0, i - 1));
72
+ } else if (key.downArrow) {
73
+ setSelectedProjectIndex((i) => Math.min(projects.length - 1, i + 1));
74
+ } else if (key.return) {
75
+ setStep("loading-deployments");
76
+ loadDeploymentsForProject(projects[selectedProjectIndex]);
77
+ } else if (key.escape) {
78
+ setStep("team");
79
+ }
80
+ }
81
+
82
+ if (step === "deploy") {
83
+ if (key.upArrow) {
84
+ setSelectedDeployIndex((i) => Math.max(0, i - 1));
85
+ } else if (key.downArrow) {
86
+ setSelectedDeployIndex((i) => Math.min(deployments.length - 1, i + 1));
87
+ } else if (key.return) {
88
+ setTargetDeployment(deployments[selectedDeployIndex]);
89
+ setStep("confirm");
90
+ } else if (key.escape) {
91
+ setStep("project");
92
+ }
93
+ }
94
+
95
+ if (step === "confirm") {
96
+ if (input === "y" || input === "Y" || key.return) {
97
+ performRollback(targetDeployment.id, targetProject.id);
98
+ } else if (input === "n" || input === "N" || key.escape) {
99
+ setStep("deploy");
100
+ }
101
+ }
102
+ });
103
+
104
+ useEffect(() => {
105
+ if (!isLoggedIn()) {
106
+ setErrorMsg("Not logged in. Run 'onflyt login' first.");
107
+ setStep("error");
108
+ return;
109
+ }
110
+
111
+ if (deploymentId) {
112
+ setErrorMsg(
113
+ "Direct deployment rollback not yet supported. Please select from list.",
114
+ );
115
+ setStep("error");
116
+ return;
117
+ }
118
+
119
+ loadTeams();
120
+ }, []);
121
+
122
+ const loadTeams = async () => {
123
+ try {
124
+ const config = getConfig();
125
+ api.setToken(config.token!);
126
+ const meData = await api.get<any>("/auth/me");
127
+ const userTeams = meData.teams || [];
128
+
129
+ if (userTeams.length === 0) {
130
+ setErrorMsg("No teams found");
131
+ setStep("error");
132
+ return;
133
+ }
134
+
135
+ setTeams(userTeams);
136
+
137
+ if (userTeams.length === 1) {
138
+ setSelectedTeamIndex(0);
139
+ setStep("loading-projects");
140
+ loadProjects(userTeams[0].team.id);
141
+ } else {
142
+ setStep("team");
143
+ }
144
+ } catch (err: any) {
145
+ setErrorMsg(err.message);
146
+ setStep("error");
147
+ }
148
+ };
149
+
150
+ const loadProjects = async (teamId: string) => {
151
+ try {
152
+ const projectsRes = await api.get<any>(`/projects/team/${teamId}`);
153
+ const teamProjects = projectsRes.projects || [];
154
+
155
+ if (teamProjects.length === 0) {
156
+ setErrorMsg("No projects found in this team");
157
+ setStep("error");
158
+ return;
159
+ }
160
+
161
+ setProjects(teamProjects);
162
+ setSelectedProjectIndex(0);
163
+ setStep("project");
164
+ } catch (err: any) {
165
+ setErrorMsg(err.message);
166
+ setStep("error");
167
+ }
168
+ };
169
+
170
+ const loadDeploymentsForProject = async (project: any) => {
171
+ try {
172
+ setTargetProject(project);
173
+ const depsRes = await api.get<any>(`/deployments/${project.id}?limit=50`);
174
+ const allDeployments = depsRes.deployments || [];
175
+
176
+ if (allDeployments.length === 0) {
177
+ setErrorMsg("No deployments found for this project");
178
+ setStep("error");
179
+ return;
180
+ }
181
+
182
+ setDeployments(allDeployments);
183
+ setSelectedDeployIndex(0);
184
+ setStep("deploy");
185
+ } catch (err: any) {
186
+ setErrorMsg(err.message);
187
+ setStep("error");
188
+ }
189
+ };
190
+
191
+ const performRollback = async (depId: string, projectId: string) => {
192
+ setStep("rolling");
193
+ try {
194
+ await api.post(`/deployments/${projectId}/${depId}/activate`);
195
+ setStep("done");
196
+ } catch (err: any) {
197
+ setErrorMsg(err.message);
198
+ setStep("error");
199
+ }
200
+ };
201
+
202
+ if (step === "loading") {
203
+ return (
204
+ <Box flexDirection="column">
205
+ <Logo />
206
+ <Box marginTop={1}>
207
+ <Text>Loading...</Text>
208
+ </Box>
209
+ </Box>
210
+ );
211
+ }
212
+
213
+ if (step === "loading-projects") {
214
+ return (
215
+ <Box flexDirection="column" padding={1}>
216
+ <Logo />
217
+ <Box marginTop={1}>
218
+ <Text bold>Rollback Deployment</Text>
219
+ </Box>
220
+ <Box marginTop={1}>
221
+ <Text dimColor>Loading projects...</Text>
222
+ </Box>
223
+ <Box marginTop={1}>
224
+ <Spinner />
225
+ </Box>
226
+ </Box>
227
+ );
228
+ }
229
+
230
+ if (step === "loading-deployments") {
231
+ return (
232
+ <Box flexDirection="column" padding={1}>
233
+ <Logo />
234
+ <Box marginTop={1}>
235
+ <Text bold>Rollback Deployment</Text>
236
+ </Box>
237
+ <Box marginTop={1}>
238
+ <Text dimColor>Loading deployments...</Text>
239
+ </Box>
240
+ <Box marginTop={1}>
241
+ <Spinner />
242
+ </Box>
243
+ </Box>
244
+ );
245
+ }
246
+
247
+ if (step === "error") {
248
+ return (
249
+ <Box flexDirection="column">
250
+ <Logo />
251
+ <Box marginTop={1}>
252
+ <Text bold color="red">
253
+ ✖ Error
254
+ </Text>
255
+ </Box>
256
+ <Box marginTop={1}>
257
+ <Text color="red">{errorMsg}</Text>
258
+ </Box>
259
+ </Box>
260
+ );
261
+ }
262
+
263
+ if (step === "rolling") {
264
+ return (
265
+ <Box flexDirection="column">
266
+ <Logo />
267
+ <Box marginTop={1}>
268
+ <Text>Rolling back deployment...</Text>
269
+ </Box>
270
+ </Box>
271
+ );
272
+ }
273
+
274
+ if (step === "done") {
275
+ return (
276
+ <Box flexDirection="column">
277
+ <Logo />
278
+ <Box marginTop={1}>
279
+ <Text bold color="green">
280
+ ✓ Rollback successful
281
+ </Text>
282
+ </Box>
283
+ <Box marginTop={1}>
284
+ <Text dimColor>Run 'onflyt logs' to monitor progress</Text>
285
+ </Box>
286
+ </Box>
287
+ );
288
+ }
289
+
290
+ if (step === "team") {
291
+ return (
292
+ <Box flexDirection="column" padding={1}>
293
+ <Logo />
294
+ <Box marginTop={1}>
295
+ <Text bold>Rollback Deployment</Text>
296
+ </Box>
297
+ <Box marginTop={1}>
298
+ <Text dimColor>
299
+ Step 1/3: Select Team (↑↓ navigate, Enter select)
300
+ </Text>
301
+ </Box>
302
+
303
+ <Box marginTop={1} flexDirection="column">
304
+ {teams.map((t, idx) => (
305
+ <Box key={t.team.id} marginTop={1}>
306
+ <Text color={idx === selectedTeamIndex ? "cyan" : "gray"}>
307
+ {idx === selectedTeamIndex ? "▶ " : " "}
308
+ </Text>
309
+ <Text bold={idx === selectedTeamIndex}>{t.team.name}</Text>
310
+ </Box>
311
+ ))}
312
+ </Box>
313
+ </Box>
314
+ );
315
+ }
316
+
317
+ if (step === "project") {
318
+ return (
319
+ <Box flexDirection="column" padding={1}>
320
+ <Logo />
321
+ <Box marginTop={1}>
322
+ <Text bold>Rollback Deployment</Text>
323
+ </Box>
324
+ <Box marginTop={1}>
325
+ <Text dimColor>
326
+ Step 2/3: Select Project - {teams[selectedTeamIndex]?.team.name}
327
+ </Text>
328
+ </Box>
329
+ <Box marginTop={1}>
330
+ <Text dimColor>(↑↓ navigate, Enter select, Esc go back)</Text>
331
+ </Box>
332
+
333
+ <Box marginTop={1} flexDirection="column">
334
+ {projects.map((p, idx) => (
335
+ <Box key={p.id} marginTop={1}>
336
+ <Text color={idx === selectedProjectIndex ? "cyan" : "gray"}>
337
+ {idx === selectedProjectIndex ? "▶ " : " "}
338
+ </Text>
339
+ <Text bold={idx === selectedProjectIndex}>{p.name}</Text>
340
+ </Box>
341
+ ))}
342
+ </Box>
343
+ </Box>
344
+ );
345
+ }
346
+
347
+ if (step === "deploy") {
348
+ const formatDate = (dateStr: string) => {
349
+ const date = new Date(dateStr);
350
+ return date.toLocaleDateString() + " " + date.toLocaleTimeString();
351
+ };
352
+
353
+ const activeDepId = targetProject?.activeDeploymentId;
354
+
355
+ return (
356
+ <Box flexDirection="column" padding={1}>
357
+ <Logo />
358
+ <Box marginTop={1}>
359
+ <Text bold>Rollback Deployment</Text>
360
+ </Box>
361
+ <Box marginTop={1}>
362
+ <Text dimColor>
363
+ Step 3/3: Select Deployment - {targetProject?.name}
364
+ </Text>
365
+ </Box>
366
+ <Box marginTop={1}>
367
+ <Text dimColor>(↑↓ navigate, Enter select, Esc go back)</Text>
368
+ </Box>
369
+
370
+ <Box marginTop={1} flexDirection="column">
371
+ {deployments.map((dep, idx) => {
372
+ const isActive = dep.id === activeDepId;
373
+ const statusColor = isActive
374
+ ? "green"
375
+ : dep.status === "live"
376
+ ? "cyan"
377
+ : "gray";
378
+ const statusLabel = isActive
379
+ ? " [ACTIVE]"
380
+ : dep.status === "live"
381
+ ? " [DEPLOYED]"
382
+ : dep.status === "failed"
383
+ ? " [FAILED]"
384
+ : ` [${dep.status.toUpperCase()}]`;
385
+
386
+ return (
387
+ <Box key={dep.id} marginTop={1} flexDirection="column">
388
+ <Box>
389
+ <Text color={idx === selectedDeployIndex ? "cyan" : "gray"}>
390
+ {idx === selectedDeployIndex ? "▶ " : " "}
391
+ </Text>
392
+ <Text bold={idx === selectedDeployIndex}>
393
+ {dep.commitMessage || "Manual Upload"}
394
+ </Text>
395
+ <Text color={statusColor}>{statusLabel}</Text>
396
+ </Box>
397
+ <Box marginLeft={2}>
398
+ <Text dimColor>ID: {dep.id}</Text>
399
+ </Box>
400
+ <Box marginLeft={2}>
401
+ <Text dimColor>{formatDate(dep.createdAt)}</Text>
402
+ </Box>
403
+ </Box>
404
+ );
405
+ })}
406
+ </Box>
407
+ </Box>
408
+ );
409
+ }
410
+
411
+ if (step === "confirm") {
412
+ const isActive = targetDeployment?.id === targetProject?.activeDeploymentId;
413
+
414
+ return (
415
+ <Box flexDirection="column" padding={1}>
416
+ <Logo />
417
+ <Box marginTop={1}>
418
+ <Text bold color="yellow">
419
+ ⚠ Confirm Rollback
420
+ </Text>
421
+ </Box>
422
+ <Box marginTop={1}>
423
+ <Text>
424
+ Project: <Text bold>{targetProject?.name}</Text>
425
+ </Text>
426
+ </Box>
427
+ <Box marginTop={1}>
428
+ <Text>
429
+ Deployment:{" "}
430
+ <Text bold>
431
+ {targetDeployment?.commitMessage || "Manual Upload"}
432
+ </Text>
433
+ </Text>
434
+ </Box>
435
+ <Box marginTop={1}>
436
+ <Text dimColor>ID: {targetDeployment?.id}</Text>
437
+ </Box>
438
+ {isActive && (
439
+ <Box marginTop={1}>
440
+ <Text color="yellow">
441
+ ⚠ This is the currently active deployment
442
+ </Text>
443
+ </Box>
444
+ )}
445
+ <Box marginTop={2}>
446
+ <Text>[Y] Yes, rollback [N] Cancel</Text>
447
+ </Box>
448
+ </Box>
449
+ );
450
+ }
451
+
452
+ return null;
453
+ };
454
+
455
+ export default Rollback;
@@ -0,0 +1,113 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { api } from "../lib/api.js";
3
+ import { getConfig, isLoggedIn } from "../lib/config.js";
4
+ import { Logo, ErrorDisplay } from "../components/Loading.js";
5
+
6
+ interface Team {
7
+ teamId: string;
8
+ role: string;
9
+ team: {
10
+ id: string;
11
+ name: string;
12
+ slug: string;
13
+ createdAt: string;
14
+ plan: string;
15
+ };
16
+ }
17
+
18
+ const MAX_RETRIES = 3;
19
+
20
+ const Teams = () => {
21
+ const [state, setState] = useState<"loading" | "error" | "done">("loading");
22
+ const [errorMsg, setErrorMsg] = useState("");
23
+ const [retryCount, setRetryCount] = useState(0);
24
+ const [teams, setTeams] = useState<Team[]>([]);
25
+
26
+ useEffect(() => {
27
+ if (state !== "done") return;
28
+
29
+ console.log("\n");
30
+ console.log(" Your Teams\n");
31
+ console.log(" " + "─".repeat(50));
32
+
33
+ if (teams.length === 0) {
34
+ console.log(" No teams found. Create a team from the dashboard.");
35
+ console.log("");
36
+ return;
37
+ }
38
+
39
+ teams.forEach((team, index) => {
40
+ const isDefault = index === 0;
41
+ console.log(
42
+ ` ${index + 1}. ${team.team.name} ${isDefault ? "(default)" : ""}`,
43
+ );
44
+ console.log(` Role: ${team.role}`);
45
+ console.log(` ID: ${team.team.id}`);
46
+ console.log(` Slug: ${team.team.slug}`);
47
+ console.log(` Plan: ${team.team.plan}`);
48
+ console.log(
49
+ ` Created: ${new Date(team.team.createdAt).toLocaleDateString()}`,
50
+ );
51
+ console.log("");
52
+ });
53
+
54
+ console.log(" " + "─".repeat(50));
55
+ console.log("\n Use --team flag: onflyt deploy --team tm_xxx\n");
56
+ }, [state, teams]);
57
+
58
+ useEffect(() => {
59
+ if (!isLoggedIn()) {
60
+ setErrorMsg("Not logged in. Run 'onflyt login' first.");
61
+ setState("error");
62
+ return;
63
+ }
64
+
65
+ const fetchTeams = async () => {
66
+ let attempt = 0;
67
+
68
+ const attemptFetch = async (): Promise<any> => {
69
+ attempt++;
70
+ try {
71
+ console.log(
72
+ `\n Fetching teams...${attempt > 1 ? ` (retry ${attempt}/${MAX_RETRIES})` : ""}\n`,
73
+ );
74
+
75
+ const config = getConfig();
76
+ api.setToken(config.token!);
77
+ const meData = await api.get<any>("/auth/me");
78
+ const userTeams: Team[] = meData.teams || [];
79
+ setTeams(userTeams);
80
+ setState("done");
81
+ return meData;
82
+ } catch (err: any) {
83
+ if (attempt < MAX_RETRIES) {
84
+ setRetryCount(attempt);
85
+ return attemptFetch();
86
+ }
87
+ throw err;
88
+ }
89
+ };
90
+
91
+ try {
92
+ await attemptFetch();
93
+ } catch (err: any) {
94
+ setErrorMsg(err.message || "Failed to fetch teams");
95
+ setState("error");
96
+ }
97
+ };
98
+
99
+ fetchTeams();
100
+ }, []);
101
+
102
+ if (state === "loading") {
103
+ return null;
104
+ }
105
+
106
+ if (state === "error") {
107
+ return <ErrorDisplay message={errorMsg} />;
108
+ }
109
+
110
+ return null;
111
+ };
112
+
113
+ export default Teams;
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { Text, Box } from "ink";
3
+ import { getConfig, isLoggedIn } from "../lib/config.js";
4
+ import { Logo, ErrorDisplay } from "../components/Loading.js";
5
+
6
+ const WhoAmI: React.FC = () => {
7
+ const config = getConfig();
8
+
9
+ if (!isLoggedIn()) {
10
+ return <ErrorDisplay message="Not logged in. Run 'onflyt login' first." />;
11
+ }
12
+
13
+ const { user, token, lastLogin } = config;
14
+
15
+ return (
16
+ <Box flexDirection="column" padding={1}>
17
+ <Logo />
18
+ <Box marginTop={1}>
19
+ <Text bold>Logged in as:</Text>
20
+ </Box>
21
+ <Box marginTop={1}>
22
+ <Text>Name: </Text>
23
+ <Text bold>{user?.name || "Unknown"}</Text>
24
+ </Box>
25
+ <Box marginTop={1}>
26
+ <Text>Email: </Text>
27
+ <Text>{user?.email}</Text>
28
+ </Box>
29
+ {user?.avatar && (
30
+ <Box marginTop={1}>
31
+ <Text dimColor>Avatar: {user.avatar}</Text>
32
+ </Box>
33
+ )}
34
+ <Box marginTop={1}>
35
+ <Text dimColor>User ID: {user?.id}</Text>
36
+ </Box>
37
+ {lastLogin && (
38
+ <Box marginTop={1}>
39
+ <Text dimColor>
40
+ Last login: {new Date(lastLogin).toLocaleString()}
41
+ </Text>
42
+ </Box>
43
+ )}
44
+ </Box>
45
+ );
46
+ };
47
+
48
+ export default WhoAmI;
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { Text, Box } from "ink";
3
+
4
+ const bigText = (str: string) => {
5
+ return str
6
+ .split("")
7
+ .map((c) => {
8
+ const code = c.toUpperCase().charCodeAt(0);
9
+ if (code >= 65 && code <= 90) {
10
+ return String.fromCharCode(0xff21 + code - 65);
11
+ }
12
+ return c;
13
+ })
14
+ .join("");
15
+ };
16
+
17
+ export const Logo: React.FC = () => (
18
+ <Box flexDirection="column">
19
+ <Box alignItems="center">
20
+ <Text color="rgb(255,191,0)"> ⬡ </Text>
21
+ <Text bold color="rgb(255,191,0)">
22
+ {bigText("Onflyt")}
23
+ </Text>
24
+ <Text> </Text>
25
+ <Text bold color="black" backgroundColor="rgb(255,191,0)">
26
+ {" "}
27
+ v0.1.0-beta{" "}
28
+ </Text>
29
+ </Box>
30
+ </Box>
31
+ );
32
+ interface LoadingProps {
33
+ message: string;
34
+ }
35
+
36
+ export const Loading: React.FC<LoadingProps> = ({ message }) => (
37
+ <Box flexDirection="column">
38
+ <Logo />
39
+ <Box marginTop={1}>
40
+ <Text>{message}</Text>
41
+ </Box>
42
+ </Box>
43
+ );
44
+
45
+ export const ErrorDisplay: React.FC<{ message: string }> = ({ message }) => (
46
+ <Box flexDirection="column">
47
+ <Logo />
48
+ <Box marginTop={1}>
49
+ <Text bold color="red">
50
+ ✖ Error
51
+ </Text>
52
+ </Box>
53
+ <Box marginTop={1}>
54
+ <Text color="red">{message}</Text>
55
+ </Box>
56
+ </Box>
57
+ );
58
+
59
+ export const Success: React.FC<{ message: string }> = ({ message }) => (
60
+ <Box flexDirection="column">
61
+ <Logo />
62
+ <Box marginTop={1}>
63
+ <Text bold color="green">
64
+ ✓ {message}
65
+ </Text>
66
+ </Box>
67
+ </Box>
68
+ );