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.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "onflyt-cli",
3
+ "version": "0.1.0-beta",
4
+ "type": "module",
5
+ "description": "Onflyt CLI - Deploy Node.js and Python APIs to serverless pods. Pay only per use.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/miiglu/onflyt-cli.git"
9
+ },
10
+ "main": "dist/index.js",
11
+ "bin": {
12
+ "onflyt": "dist/index.js"
13
+ },
14
+ "scripts": {
15
+ "dev": "tsx src/index.tsx",
16
+ "build": "esbuild src/index.tsx --bundle --platform=node --outfile=dist/index.js --format=esm --external:react-devtools-core --external:ink --external:react --external:react-dom --external:figlet",
17
+ "start": "node dist/index.js",
18
+ "pack": "npm pack --pack-destination dist"
19
+ },
20
+ "keywords": [
21
+ "cli",
22
+ "deployment",
23
+ "serverless",
24
+ "nodejs",
25
+ "python",
26
+ "api",
27
+ "serverless-pods",
28
+ "typescript",
29
+ "react",
30
+ "ink",
31
+ "cloudflare",
32
+ "cloud",
33
+ "devops",
34
+ "onflyt"
35
+ ],
36
+ "author": "Miiglu",
37
+ "license": "Apache-2.0",
38
+ "dependencies": {
39
+ "@octokit/oauth-methods": "^6.0.2",
40
+ "archiver": "^7.0.1",
41
+ "figlet": "^1.11.0",
42
+ "ink": "^6.8.0",
43
+ "ink-select-input": "^6.2.0",
44
+ "ink-spinner": "^5.0.0",
45
+ "ink-text-input": "^6.0.0",
46
+ "meow": "^14.1.0",
47
+ "open": "^11.0.0",
48
+ "react": "^19.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/archiver": "^7.0.0",
52
+ "@types/node": "^25.5.2",
53
+ "@types/react": "^19.2.7",
54
+ "esbuild": "^0.25.0",
55
+ "tsx": "^4.21.0",
56
+ "typescript": "^6.0.2"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.0.0"
60
+ }
61
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { Text, Box } from "ink";
3
+
4
+ const App = () => {
5
+ return (
6
+ <Box flexDirection="column">
7
+ <Text bold> ⬡ Onflyt CLI v0.1.0-beta</Text>
8
+ <Text>Type onflyt --help for available commands</Text>
9
+ </Box>
10
+ );
11
+ };
12
+
13
+ export default App;
@@ -0,0 +1,151 @@
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
+ plan?: string;
14
+ };
15
+ }
16
+
17
+ interface Balance {
18
+ balanceUSD: number;
19
+ balanceFormatted: string;
20
+ }
21
+
22
+ interface CreditDisplay {
23
+ team: Team;
24
+ balance: Balance;
25
+ }
26
+
27
+ const MAX_RETRIES = 3;
28
+
29
+ const Credits = () => {
30
+ const [state, setState] = useState<"loading" | "error" | "done">("loading");
31
+ const [errorMsg, setErrorMsg] = useState("");
32
+ const [retryCount, setRetryCount] = useState(0);
33
+ const [credits, setCredits] = useState<CreditDisplay[]>([]);
34
+
35
+ useEffect(() => {
36
+ if (state !== "done") return;
37
+
38
+ console.log("\n Credits Balance\n");
39
+ console.log(" " + "─".repeat(50));
40
+
41
+ credits.forEach((item) => {
42
+ const { team, balance } = item;
43
+ const isLow = balance.balanceUSD < 5;
44
+ const plan = team.team.plan || "free";
45
+
46
+ console.log(`\n ${team.team.name} (${plan})`);
47
+
48
+ if (isLow) {
49
+ console.log(
50
+ ` \x1b[33m Credits: ${balance.balanceFormatted} ⚠ Low credits\x1b[0m`,
51
+ );
52
+ } else {
53
+ console.log(
54
+ ` \x1b[32m Credits: ${balance.balanceFormatted} ✓\x1b[0m`,
55
+ );
56
+ }
57
+ });
58
+
59
+ console.log("\n " + "─".repeat(50));
60
+ console.log("\n Run 'onflyt add-credits' to add more credits.\n");
61
+ }, [state, credits]);
62
+
63
+ useEffect(() => {
64
+ if (!isLoggedIn()) {
65
+ setErrorMsg("Not logged in. Run 'onflyt login' first.");
66
+ setState("error");
67
+ return;
68
+ }
69
+
70
+ const fetchCredits = async () => {
71
+ let attempt = 0;
72
+
73
+ const attemptFetch = async () => {
74
+ attempt++;
75
+ try {
76
+ console.log(
77
+ `\n Fetching credits...${attempt > 1 ? ` (retry ${attempt}/${MAX_RETRIES})` : ""}\n`,
78
+ );
79
+
80
+ const config = getConfig();
81
+ api.setToken(config.token!);
82
+
83
+ const meData = await api.get<any>("/auth/me");
84
+ const userTeams: Team[] = meData.teams || [];
85
+
86
+ if (userTeams.length === 0) {
87
+ console.log("\n No teams found.\n");
88
+ console.log(" You don't have any teams yet.\n");
89
+ return;
90
+ }
91
+
92
+ const balancesData: Record<string, Balance> = {};
93
+
94
+ await Promise.all(
95
+ userTeams.map(async (team: Team) => {
96
+ try {
97
+ const balanceData = await api.get<any>(
98
+ `/billing/balance?teamId=${team.team.id}`,
99
+ );
100
+ balancesData[team.team.id] = balanceData.data || balanceData;
101
+ } catch {
102
+ balancesData[team.team.id] = {
103
+ balanceUSD: 0,
104
+ balanceFormatted: "$0.00",
105
+ };
106
+ }
107
+ }),
108
+ );
109
+
110
+ const creditDisplay: CreditDisplay[] = userTeams.map((team) => ({
111
+ team,
112
+ balance: balancesData[team.team.id] || {
113
+ balanceUSD: 0,
114
+ balanceFormatted: "$0.00",
115
+ },
116
+ }));
117
+
118
+ setCredits(creditDisplay);
119
+ setState("done");
120
+ } catch (err) {
121
+ if (attempt < MAX_RETRIES) {
122
+ setRetryCount(attempt);
123
+ return attemptFetch();
124
+ }
125
+ throw err;
126
+ }
127
+ };
128
+
129
+ try {
130
+ await attemptFetch();
131
+ } catch (err: any) {
132
+ setErrorMsg(err.message || "Failed to fetch credits");
133
+ setState("error");
134
+ }
135
+ };
136
+
137
+ fetchCredits();
138
+ }, []);
139
+
140
+ if (state === "loading") {
141
+ return null;
142
+ }
143
+
144
+ if (state === "error") {
145
+ return <ErrorDisplay message={errorMsg} />;
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ export default Credits;
@@ -0,0 +1,315 @@
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 DeleteProps {
9
+ projectId?: string;
10
+ projectName?: string;
11
+ }
12
+
13
+ const Delete: React.FC<DeleteProps> = ({ projectId, projectName }) => {
14
+ const [step, setStep] = useState<
15
+ | "loading"
16
+ | "loading-projects"
17
+ | "team"
18
+ | "select"
19
+ | "confirm"
20
+ | "deleting"
21
+ | "done"
22
+ | "error"
23
+ >("loading");
24
+
25
+ const [teams, setTeams] = useState<any[]>([]);
26
+ const [projects, setProjects] = useState<any[]>([]);
27
+ const [selectedTeamIndex, setSelectedTeamIndex] = useState(0);
28
+ const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
29
+ const [targetProject, setTargetProject] = useState<any>(null);
30
+ const [errorMsg, setErrorMsg] = useState("");
31
+
32
+ useEffect(() => {
33
+ const handleSigInt = () => process.exit(0);
34
+ process.on("SIGINT", handleSigInt);
35
+ return () => {
36
+ process.off("SIGINT", handleSigInt);
37
+ };
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ if (step === "done" || step === "error") {
42
+ const timer = setTimeout(() => process.exit(0), 500);
43
+ return () => clearTimeout(timer);
44
+ }
45
+ }, [step]);
46
+
47
+ useInput((input, key) => {
48
+ if (input === "q" || input === "Q" || (key.ctrl && input === "c")) {
49
+ process.exit(0);
50
+ }
51
+
52
+ if (step === "team") {
53
+ if (key.upArrow) {
54
+ setSelectedTeamIndex((i) => Math.max(0, i - 1));
55
+ } else if (key.downArrow) {
56
+ setSelectedTeamIndex((i) => Math.min(teams.length - 1, i + 1));
57
+ } else if (key.return) {
58
+ setStep("loading-projects");
59
+ loadProjects(teams[selectedTeamIndex].team.id);
60
+ }
61
+ }
62
+
63
+ if (step === "select") {
64
+ if (key.upArrow) {
65
+ setSelectedProjectIndex((i) => Math.max(0, i - 1));
66
+ } else if (key.downArrow) {
67
+ setSelectedProjectIndex((i) => Math.min(projects.length - 1, i + 1));
68
+ } else if (key.return) {
69
+ setTargetProject(projects[selectedProjectIndex]);
70
+ setStep("confirm");
71
+ } else if (key.escape) {
72
+ setStep("team");
73
+ }
74
+ }
75
+
76
+ if (step === "confirm") {
77
+ if (input === "y" || input === "Y" || key.return) {
78
+ deleteProject(targetProject.id);
79
+ } else if (input === "n" || input === "N" || key.escape) {
80
+ setStep("select");
81
+ }
82
+ }
83
+ });
84
+
85
+ useEffect(() => {
86
+ if (!isLoggedIn()) {
87
+ setErrorMsg("Not logged in. Run 'onflyt login' first.");
88
+ setStep("error");
89
+ return;
90
+ }
91
+
92
+ if (projectId) {
93
+ setTargetProject({ id: projectId, name: projectName || projectId });
94
+ setStep("confirm");
95
+ return;
96
+ }
97
+
98
+ loadTeams();
99
+ }, []);
100
+
101
+ const loadTeams = async () => {
102
+ try {
103
+ const config = getConfig();
104
+ api.setToken(config.token!);
105
+ const meData = await api.get<any>("/auth/me");
106
+ const userTeams = meData.teams || [];
107
+
108
+ if (userTeams.length === 0) {
109
+ setErrorMsg("No teams found");
110
+ setStep("error");
111
+ return;
112
+ }
113
+
114
+ setTeams(userTeams);
115
+
116
+ if (userTeams.length === 1) {
117
+ setSelectedTeamIndex(0);
118
+ setStep("loading-projects");
119
+ loadProjects(userTeams[0].team.id);
120
+ } else {
121
+ setStep("team");
122
+ }
123
+ } catch (err: any) {
124
+ setErrorMsg(err.message);
125
+ setStep("error");
126
+ }
127
+ };
128
+
129
+ const loadProjects = async (teamId: string) => {
130
+ try {
131
+ const projectsRes = await api.get<any>(`/projects/team/${teamId}`);
132
+ const teamProjects = projectsRes.projects || [];
133
+
134
+ if (teamProjects.length === 0) {
135
+ setErrorMsg("No projects found in this team");
136
+ setStep("error");
137
+ return;
138
+ }
139
+
140
+ setProjects(teamProjects);
141
+ setSelectedProjectIndex(0);
142
+ setStep("select");
143
+ } catch (err: any) {
144
+ setErrorMsg(err.message);
145
+ setStep("error");
146
+ }
147
+ };
148
+
149
+ const deleteProject = async (projId: string) => {
150
+ setStep("deleting");
151
+ try {
152
+ await api.delete(`/projects/${projId}`);
153
+ setStep("done");
154
+ } catch (err: any) {
155
+ setErrorMsg(err.message);
156
+ setStep("error");
157
+ }
158
+ };
159
+
160
+ if (step === "loading") {
161
+ return (
162
+ <Box flexDirection="column">
163
+ <Logo />
164
+ <Box marginTop={1}>
165
+ <Text>Loading...</Text>
166
+ </Box>
167
+ </Box>
168
+ );
169
+ }
170
+
171
+ if (step === "loading-projects") {
172
+ return (
173
+ <Box flexDirection="column" padding={1}>
174
+ <Logo />
175
+ <Box marginTop={1}>
176
+ <Text bold>Delete Project</Text>
177
+ </Box>
178
+ <Box marginTop={1}>
179
+ <Text dimColor>Loading projects...</Text>
180
+ </Box>
181
+ <Box marginTop={1}>
182
+ <Text>
183
+ <Spinner />
184
+ </Text>
185
+ </Box>
186
+ </Box>
187
+ );
188
+ }
189
+
190
+ if (step === "error") {
191
+ return (
192
+ <Box flexDirection="column">
193
+ <Logo />
194
+ <Box marginTop={1}>
195
+ <Text bold color="red">
196
+ ✖ Error
197
+ </Text>
198
+ </Box>
199
+ <Box marginTop={1}>
200
+ <Text color="red">{errorMsg}</Text>
201
+ </Box>
202
+ </Box>
203
+ );
204
+ }
205
+
206
+ if (step === "deleting") {
207
+ return (
208
+ <Box flexDirection="column">
209
+ <Logo />
210
+ <Box marginTop={1}>
211
+ <Text>Deleting project...</Text>
212
+ </Box>
213
+ </Box>
214
+ );
215
+ }
216
+
217
+ if (step === "done") {
218
+ return (
219
+ <Box flexDirection="column">
220
+ <Logo />
221
+ <Box marginTop={1}>
222
+ <Text bold color="green">
223
+ ✓ Project deleted successfully
224
+ </Text>
225
+ </Box>
226
+ </Box>
227
+ );
228
+ }
229
+
230
+ if (step === "team") {
231
+ return (
232
+ <Box flexDirection="column" padding={1}>
233
+ <Logo />
234
+ <Box marginTop={1}>
235
+ <Text bold>Delete Project</Text>
236
+ </Box>
237
+ <Box marginTop={1}>
238
+ <Text dimColor>
239
+ Step 1/2: Select Team (↑↓ navigate, Enter select)
240
+ </Text>
241
+ </Box>
242
+
243
+ <Box marginTop={1} flexDirection="column">
244
+ {teams.map((t, idx) => (
245
+ <Box key={t.team.id} marginTop={1}>
246
+ <Text color={idx === selectedTeamIndex ? "cyan" : "gray"}>
247
+ {idx === selectedTeamIndex ? "▶ " : " "}
248
+ </Text>
249
+ <Text bold={idx === selectedTeamIndex}>{t.team.name}</Text>
250
+ </Box>
251
+ ))}
252
+ </Box>
253
+ </Box>
254
+ );
255
+ }
256
+
257
+ if (step === "select") {
258
+ return (
259
+ <Box flexDirection="column" padding={1}>
260
+ <Logo />
261
+ <Box marginTop={1}>
262
+ <Text bold>Delete Project</Text>
263
+ </Box>
264
+ <Box marginTop={1}>
265
+ <Text dimColor>
266
+ Step 2/2: Select Project - {teams[selectedTeamIndex]?.team.name}
267
+ </Text>
268
+ </Box>
269
+ <Box>
270
+ <Text dimColor>(↑↓ navigate, Enter select, Esc go back)</Text>
271
+ </Box>
272
+
273
+ <Box marginTop={1} flexDirection="column">
274
+ {projects.map((p, idx) => (
275
+ <Box key={p.id} marginTop={1}>
276
+ <Text color={idx === selectedProjectIndex ? "cyan" : "gray"}>
277
+ {idx === selectedProjectIndex ? "▶ " : " "}
278
+ </Text>
279
+ <Text bold={idx === selectedProjectIndex}>{p.name}</Text>
280
+ <Text dimColor> ({p.framework})</Text>
281
+ </Box>
282
+ ))}
283
+ </Box>
284
+ </Box>
285
+ );
286
+ }
287
+
288
+ if (step === "confirm") {
289
+ return (
290
+ <Box flexDirection="column" padding={1}>
291
+ <Logo />
292
+ <Box marginTop={1}>
293
+ <Text bold color="red">
294
+ ⚠ Confirm Delete
295
+ </Text>
296
+ </Box>
297
+ <Box marginTop={1}>
298
+ <Text>
299
+ Delete project: <Text bold>{targetProject?.name}</Text>?
300
+ </Text>
301
+ </Box>
302
+ <Box marginTop={1}>
303
+ <Text color="red">This action cannot be undone!</Text>
304
+ </Box>
305
+ <Box marginTop={2}>
306
+ <Text>[Y] Yes, delete [N] Cancel</Text>
307
+ </Box>
308
+ </Box>
309
+ );
310
+ }
311
+
312
+ return null;
313
+ };
314
+
315
+ export default Delete;