onflyt-cli 1.0.1-beta.2 → 1.0.1-beta.3

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.

Files changed (67) hide show
  1. package/dist/App.d.ts +3 -0
  2. package/dist/App.js +8 -0
  3. package/dist/commands/credits.d.ts +3 -0
  4. package/dist/commands/credits.js +101 -0
  5. package/dist/commands/delete.d.ts +7 -0
  6. package/dist/commands/delete.js +220 -0
  7. package/dist/commands/deploy.d.ts +6 -0
  8. package/dist/commands/deploy.js +715 -0
  9. package/dist/commands/deployments.d.ts +6 -0
  10. package/dist/commands/deployments.js +225 -0
  11. package/dist/commands/help.d.ts +3 -0
  12. package/dist/commands/help.js +76 -0
  13. package/dist/commands/init.d.ts +11 -0
  14. package/dist/commands/init.js +422 -0
  15. package/dist/commands/login.d.ts +6 -0
  16. package/dist/commands/login.js +150 -0
  17. package/dist/commands/logout.d.ts +3 -0
  18. package/dist/commands/logout.js +19 -0
  19. package/dist/commands/logs.d.ts +7 -0
  20. package/dist/commands/logs.js +307 -0
  21. package/dist/commands/projects.d.ts +3 -0
  22. package/dist/commands/projects.js +203 -0
  23. package/dist/commands/rollback.d.ts +6 -0
  24. package/dist/commands/rollback.js +316 -0
  25. package/dist/commands/teams.d.ts +3 -0
  26. package/dist/commands/teams.js +81 -0
  27. package/dist/commands/whoami.d.ts +3 -0
  28. package/dist/commands/whoami.js +34 -0
  29. package/dist/components/Loading.d.ts +13 -0
  30. package/dist/components/Loading.js +42 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +77 -116
  33. package/dist/lib/api.d.ts +27 -0
  34. package/dist/lib/api.js +109 -0
  35. package/dist/lib/config.d.ts +32 -0
  36. package/dist/lib/config.js +52 -0
  37. package/dist/lib/deploy-api.d.ts +97 -0
  38. package/dist/lib/deploy-api.js +335 -0
  39. package/dist/lib/deploy.d.ts +36 -0
  40. package/dist/lib/deploy.js +181 -0
  41. package/dist/lib/framework.d.ts +27 -0
  42. package/dist/lib/framework.js +184 -0
  43. package/dist/lib/git.d.ts +25 -0
  44. package/dist/lib/git.js +149 -0
  45. package/dist/lib/scaffold.d.ts +21 -0
  46. package/dist/lib/scaffold.js +190 -0
  47. package/dist/shared/frameworks/registry.d.ts +21 -0
  48. package/dist/shared/frameworks/registry.js +196 -0
  49. package/dist/shared/index.d.ts +4 -0
  50. package/dist/shared/index.js +4 -0
  51. package/dist/shared/limits.d.ts +16 -0
  52. package/dist/shared/limits.js +44 -0
  53. package/dist/shared/pricing.d.ts +2 -0
  54. package/dist/shared/pricing.js +7 -0
  55. package/dist/shared/templates/registry.d.ts +9 -0
  56. package/dist/shared/templates/registry.js +47 -0
  57. package/package.json +2 -3
  58. package/src/App.tsx +1 -1
  59. package/src/commands/deploy.tsx +1 -1
  60. package/src/commands/help.tsx +1 -1
  61. package/src/commands/init.tsx +1 -1
  62. package/src/commands/projects.tsx +1 -1
  63. package/src/components/Loading.tsx +1 -1
  64. package/src/index.tsx +1 -1
  65. package/src/lib/deploy-api.ts +3 -3
  66. package/src/lib/framework.ts +2 -2
  67. package/src/lib/shared.ts +350 -0
@@ -0,0 +1,715 @@
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { isLoggedIn } from "../lib/config.js";
5
+ import { GitDetector } from "../lib/git.js";
6
+ import { INSTANCE_OPTIONS, isPodProject, getProjectConfig, loadTeamsWithBalances, getTeamDetails, getTeamLimits, getTeamPlanLabel, findOrCreateProject, updateProjectSettings, getProjectDetails, startDeployment, getDeploymentStatus, streamLogs, } from "../lib/deploy-api.js";
7
+ import { TIER_HOURLY_PRICE } from "../shared";
8
+ import { api } from "../lib/api.js";
9
+ const Deploy = ({ teamFlag }) => {
10
+ const [step, setStep] = useState("loading");
11
+ const [errorMsg, setErrorMsg] = useState("");
12
+ useEffect(() => {
13
+ const handleSigInt = () => process.exit(0);
14
+ process.on("SIGINT", handleSigInt);
15
+ return () => {
16
+ process.off("SIGINT", handleSigInt);
17
+ };
18
+ }, []);
19
+ useEffect(() => {
20
+ if (step === "done" || step === "error") {
21
+ const timer = setTimeout(() => process.exit(0), 500);
22
+ return () => clearTimeout(timer);
23
+ }
24
+ }, [step]);
25
+ const [teams, setTeams] = useState([]);
26
+ const [selectedTeamIndex, setSelectedTeamIndex] = useState(0);
27
+ const [balances, setBalances] = useState({});
28
+ const [projectConfig, setProjectConfig] = useState(null);
29
+ const [selectedInstanceIndex, setSelectedInstanceIndex] = useState(2);
30
+ const [replicas, setReplicas] = useState(1);
31
+ const [envVars, setEnvVars] = useState([]);
32
+ const [envMessage, setEnvMessage] = useState("");
33
+ const [deploymentId, setDeploymentId] = useState("");
34
+ const [deploymentStatus, setDeploymentStatus] = useState("queued");
35
+ const [liveLogs, setLiveLogs] = useState([]);
36
+ const [previewUrl, setPreviewUrl] = useState("");
37
+ const [liveUrl, setLiveUrl] = useState("");
38
+ const [projectId, setProjectId] = useState("");
39
+ const [dotCount, setDotCount] = useState(0);
40
+ const [existingProject, setExistingProject] = useState(null);
41
+ const [isNewProject, setIsNewProject] = useState(false);
42
+ const [projectInstanceSize, setProjectInstanceSize] = useState("");
43
+ const [projectReplicas, setProjectReplicas] = useState(1);
44
+ const [totalLifetimeSpend, setTotalLifetimeSpend] = useState(0);
45
+ const [currentBalance, setCurrentBalance] = useState({ balanceUSD: 0, balanceFormatted: "$0.00" });
46
+ const [hasGitRemote, setHasGitRemote] = useState(false);
47
+ const [deployStage, setDeployStage] = useState("starting");
48
+ const [uploadProgress, setUploadProgress] = useState({
49
+ uploaded: 0,
50
+ total: 0,
51
+ });
52
+ const [zipSize, setZipSize] = useState(null);
53
+ const selectedTeam = teams[selectedTeamIndex];
54
+ const limits = getTeamLimits(totalLifetimeSpend);
55
+ const planLabel = getTeamPlanLabel(totalLifetimeSpend);
56
+ const needsPod = isPodProject(projectConfig?.framework);
57
+ const selectedInstance = INSTANCE_OPTIONS[selectedInstanceIndex];
58
+ const estimatedHourlyCost = needsPod && selectedInstance
59
+ ? TIER_HOURLY_PRICE[selectedInstance.id] * replicas
60
+ : null;
61
+ const formatEstimatedCost = (cost) => {
62
+ if (cost === 0)
63
+ return "Free";
64
+ return `$${cost.toFixed(3)}/hr`;
65
+ };
66
+ useEffect(() => {
67
+ if (step !== "loading")
68
+ return;
69
+ if (!isLoggedIn()) {
70
+ setErrorMsg("Not logged in. Run 'onflyt login' first.");
71
+ setStep("error");
72
+ return;
73
+ }
74
+ const config = getProjectConfig();
75
+ if (!config) {
76
+ setErrorMsg("No onflyt.json found. Run 'onflyt init' first.");
77
+ setStep("error");
78
+ return;
79
+ }
80
+ setProjectConfig(config);
81
+ const loadData = async () => {
82
+ try {
83
+ const gitDetector = new GitDetector(process.cwd());
84
+ const gitInfo = await gitDetector.detect();
85
+ setHasGitRemote(gitInfo.isGitRepo && gitInfo.remotes.length > 0);
86
+ const { teams: loadedTeams, balances: loadedBalances } = await loadTeamsWithBalances();
87
+ setTeams(loadedTeams);
88
+ setBalances(loadedBalances);
89
+ const selectedIdx = teamFlag
90
+ ? loadedTeams.findIndex((t) => t.team.id === teamFlag || t.team.slug === teamFlag)
91
+ : 0;
92
+ if (selectedIdx >= 0)
93
+ setSelectedTeamIndex(selectedIdx);
94
+ const selectedTeamForProject = loadedTeams[selectedIdx >= 0 ? selectedIdx : 0];
95
+ if (selectedTeamForProject) {
96
+ try {
97
+ const teamDetails = await getTeamDetails(selectedTeamForProject.team.id);
98
+ setTotalLifetimeSpend(teamDetails.totalLifetimeSpend);
99
+ setCurrentBalance({
100
+ balanceUSD: teamDetails.balanceUSD,
101
+ balanceFormatted: loadedBalances[selectedTeamForProject.team.id]
102
+ ?.balanceFormatted || "$0.00",
103
+ });
104
+ }
105
+ catch {
106
+ setTotalLifetimeSpend(0);
107
+ setCurrentBalance(loadedBalances[selectedTeamForProject.team.id] || {
108
+ balanceUSD: 0,
109
+ balanceFormatted: "$0.00",
110
+ });
111
+ }
112
+ try {
113
+ const projectsRes = await api.get(`/projects/team/${selectedTeamForProject.team.id}`);
114
+ const existingProject = (projectsRes.projects || []).find((p) => p.name === config.name);
115
+ if (existingProject) {
116
+ const fullProject = await getProjectDetails(existingProject.id);
117
+ setExistingProject(fullProject);
118
+ setProjectId(fullProject.id);
119
+ setIsNewProject(false);
120
+ setProjectInstanceSize(fullProject.instanceSize || "micro");
121
+ setProjectReplicas(fullProject.maxInstances || 1);
122
+ const instanceIdx = INSTANCE_OPTIONS.findIndex((i) => i.id === (fullProject.instanceSize || "micro"));
123
+ setSelectedInstanceIndex(instanceIdx >= 0 ? instanceIdx : 2);
124
+ }
125
+ else {
126
+ setIsNewProject(true);
127
+ }
128
+ }
129
+ catch {
130
+ setIsNewProject(true);
131
+ }
132
+ }
133
+ setStep("team");
134
+ }
135
+ catch (err) {
136
+ setErrorMsg(err.message || "Failed to load data");
137
+ setStep("error");
138
+ }
139
+ };
140
+ loadData();
141
+ }, [step, teamFlag]);
142
+ useEffect(() => {
143
+ if (step !== "deploying")
144
+ return;
145
+ if (deploymentStatus === "deployed" || deploymentStatus === "failed")
146
+ return;
147
+ const interval = setInterval(() => {
148
+ setDotCount((c) => (c + 1) % 4);
149
+ }, 500);
150
+ return () => clearInterval(interval);
151
+ }, [step, deploymentStatus]);
152
+ const handleDeploy = useCallback(async () => {
153
+ if (!selectedTeam || !projectConfig)
154
+ return;
155
+ try {
156
+ const project = await findOrCreateProject(selectedTeam.team.id, projectConfig.name, projectConfig.framework, projectConfig.gitRepoUrl);
157
+ if (!project.isNew) {
158
+ const fullProject = await getProjectDetails(project.id);
159
+ setExistingProject(fullProject);
160
+ setProjectId(project.id);
161
+ setIsNewProject(false);
162
+ const existingSize = fullProject.instanceSize || "micro";
163
+ const existingReplicas = fullProject.maxInstances || 1;
164
+ setProjectInstanceSize(existingSize);
165
+ setProjectReplicas(existingReplicas);
166
+ if (needsPod) {
167
+ const instanceIdx = INSTANCE_OPTIONS.findIndex((i) => i.id === existingSize);
168
+ setSelectedInstanceIndex(instanceIdx >= 0 ? instanceIdx : 2);
169
+ setReplicas(existingReplicas);
170
+ setStep("instance");
171
+ }
172
+ else {
173
+ setStep("env");
174
+ }
175
+ }
176
+ else {
177
+ setProjectId(project.id);
178
+ setIsNewProject(true);
179
+ if (needsPod) {
180
+ setStep("instance");
181
+ }
182
+ else {
183
+ setStep("env");
184
+ }
185
+ }
186
+ }
187
+ catch (err) {
188
+ setErrorMsg(err.message);
189
+ setStep("error");
190
+ }
191
+ }, [selectedTeam, projectConfig, needsPod]);
192
+ const triggerDeployment = useCallback(async (projId) => {
193
+ setStep("deploying");
194
+ setDeployStage("starting");
195
+ setUploadProgress({ uploaded: 0, total: 0 });
196
+ setZipSize(null);
197
+ const hasConfiguredGit = !!projectConfig?.gitRepoUrl;
198
+ if (hasConfiguredGit) {
199
+ try {
200
+ const depId = await startDeployment(projId, projectConfig?.gitBranch || "main", needsPod ? selectedInstance?.id : undefined, needsPod ? replicas : undefined, envVars.length > 0 ? envVars : undefined);
201
+ setDeploymentId(depId);
202
+ pollStatus(depId);
203
+ }
204
+ catch (err) {
205
+ setStep("error");
206
+ setErrorMsg(err.message);
207
+ }
208
+ return;
209
+ }
210
+ const { join } = await import("path");
211
+ const { tmpdir } = await import("os");
212
+ const { statSync } = await import("fs");
213
+ const zipPath = join(tmpdir(), `onflyt-deploy-${Date.now()}.zip`);
214
+ setDeployStage("zipping");
215
+ setTimeout(async () => {
216
+ try {
217
+ const { createProjectZip } = await import("../lib/deploy-api.js");
218
+ await createProjectZip(process.cwd(), zipPath);
219
+ const size = statSync(zipPath).size;
220
+ setZipSize(size);
221
+ console.log(`\x1b[90m Archive size: ${(size / 1024 / 1024).toFixed(1)} MB\x1b[0m`);
222
+ if (size > 100 * 1024 * 1024) {
223
+ console.log(`\x1b[33m Warning: File exceeds 100MB limit\x1b[0m`);
224
+ }
225
+ setDeployStage("uploading");
226
+ const { deployManual } = await import("../lib/deploy-api.js");
227
+ const depId = await deployManual(projId, zipPath, projectConfig?.framework, needsPod ? selectedInstance?.id : undefined, needsPod ? replicas : undefined, envVars.length > 0 ? envVars : undefined, (uploaded, total) => {
228
+ setUploadProgress({ uploaded, total });
229
+ }, {
230
+ buildCommand: projectConfig?.buildCommand,
231
+ outputDirectory: projectConfig?.outputDirectory,
232
+ installCommand: projectConfig?.installCommand,
233
+ });
234
+ setDeploymentId(depId);
235
+ pollStatus(depId);
236
+ }
237
+ catch (err) {
238
+ setStep("error");
239
+ setErrorMsg(err.message);
240
+ }
241
+ }, 100);
242
+ }, [projectConfig, selectedInstance, replicas, envVars, needsPod]);
243
+ const handleUpdateAndDeploy = useCallback(async () => {
244
+ try {
245
+ if (needsPod) {
246
+ const instanceToUse = selectedInstance?.id || projectInstanceSize;
247
+ const replicasToUse = replicas || projectReplicas;
248
+ if (instanceToUse || replicasToUse) {
249
+ await updateProjectSettings(projectId, instanceToUse, replicasToUse);
250
+ }
251
+ }
252
+ setStep("deploying");
253
+ triggerDeployment(projectId);
254
+ }
255
+ catch (err) {
256
+ setErrorMsg(err.message);
257
+ setStep("error");
258
+ }
259
+ }, [
260
+ projectId,
261
+ projectInstanceSize,
262
+ projectReplicas,
263
+ selectedInstance,
264
+ replicas,
265
+ needsPod,
266
+ triggerDeployment,
267
+ ]);
268
+ const pollStatus = useCallback((depId) => {
269
+ const poll = async () => {
270
+ try {
271
+ const details = await getDeploymentStatus(depId);
272
+ setDeploymentStatus(details.status);
273
+ if (details.status === "building" && liveLogs.length === 0) {
274
+ if (limits.enableLogStreaming) {
275
+ streamLogs(depId, (log) => {
276
+ setLiveLogs((prev) => [...prev.slice(-200), log]);
277
+ }, () => {
278
+ console.log("\x1b[33m⚠ Live logs unavailable\x1b[0m");
279
+ });
280
+ }
281
+ }
282
+ if (details.status === "live") {
283
+ setLiveUrl(details.url || `https://${projectConfig?.name}.onflyt.dev`);
284
+ setPreviewUrl(details.previewUrl || "");
285
+ setStep("done");
286
+ return;
287
+ }
288
+ if (details.status === "failed") {
289
+ setErrorMsg("Deployment failed. Run 'onflyt logs " + depId + "' for details.");
290
+ setStep("error");
291
+ return;
292
+ }
293
+ setTimeout(() => poll(), 3000);
294
+ }
295
+ catch (err) {
296
+ setTimeout(() => poll(), 5000);
297
+ }
298
+ };
299
+ poll();
300
+ }, [liveLogs.length, limits.enableLogStreaming, projectConfig?.name]);
301
+ useInput((input, key) => {
302
+ if (input === "q" || input === "Q" || (key.ctrl && input === "c")) {
303
+ process.exit(0);
304
+ }
305
+ if (key.escape) {
306
+ if (step === "instance")
307
+ setStep("config");
308
+ if (step === "env")
309
+ setStep(needsPod ? "instance" : "config");
310
+ return;
311
+ }
312
+ if (key.return) {
313
+ if (step === "team") {
314
+ setStep("config");
315
+ }
316
+ else if (step === "config") {
317
+ handleDeploy();
318
+ }
319
+ else if (step === "instance") {
320
+ setStep("env");
321
+ }
322
+ else if (step === "env") {
323
+ triggerDeployment(projectId);
324
+ }
325
+ return;
326
+ }
327
+ if (step === "config" && input === "1" && needsPod) {
328
+ setStep("instance");
329
+ return;
330
+ }
331
+ if (step === "team") {
332
+ if (key.upArrow) {
333
+ setSelectedTeamIndex((i) => Math.max(0, i - 1));
334
+ }
335
+ else if (key.downArrow) {
336
+ setSelectedTeamIndex((i) => Math.min(teams.length - 1, i + 1));
337
+ }
338
+ return;
339
+ }
340
+ if (step === "instance") {
341
+ if (key.upArrow) {
342
+ setSelectedInstanceIndex((i) => Math.max(0, i - 1));
343
+ }
344
+ else if (key.downArrow) {
345
+ setSelectedInstanceIndex((i) => Math.min(INSTANCE_OPTIONS.length - 1, i + 1));
346
+ }
347
+ else if (key.leftArrow) {
348
+ setReplicas((r) => Math.max(1, r - 1));
349
+ }
350
+ else if (key.rightArrow) {
351
+ setReplicas((r) => Math.min(INSTANCE_OPTIONS[selectedInstanceIndex].maxReplicas, r + 1));
352
+ }
353
+ return;
354
+ }
355
+ if (step === "env") {
356
+ if (input === "1") {
357
+ handleImportEnv();
358
+ }
359
+ else if (input === "2") {
360
+ handleAddEnv();
361
+ }
362
+ else if (input === "3") {
363
+ handleAddEnv();
364
+ }
365
+ return;
366
+ }
367
+ if (input === "q" || input === "Q") {
368
+ process.exit(0);
369
+ }
370
+ });
371
+ const handleImportEnv = () => {
372
+ try {
373
+ const projectPath = process.cwd();
374
+ const envPath = `${projectPath}/.env`;
375
+ if (existsSync(envPath)) {
376
+ const content = readFileSync(envPath, "utf-8");
377
+ const vars = content
378
+ .split("\n")
379
+ .filter((line) => line.includes("=") && !line.startsWith("#"))
380
+ .map((line) => {
381
+ const [key, ...rest] = line.split("=");
382
+ return { key: key.trim(), value: rest.join("=").trim() };
383
+ });
384
+ setEnvVars(vars);
385
+ }
386
+ }
387
+ catch { }
388
+ handleUpdateAndDeploy();
389
+ };
390
+ const handleAddEnv = () => {
391
+ handleUpdateAndDeploy();
392
+ };
393
+ if (step === "loading") {
394
+ return (React.createElement(Box, { flexDirection: "column" },
395
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
396
+ React.createElement(Text, null, "DEPLOY")),
397
+ React.createElement(Box, { marginTop: 1 },
398
+ React.createElement(Text, null, "Loading..."))));
399
+ }
400
+ if (step === "team") {
401
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
402
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
403
+ React.createElement(Text, null, "DEPLOY")),
404
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
405
+ React.createElement(Text, { bold: true }, "Select Team"),
406
+ React.createElement(Text, { dimColor: true }, "Use \u2191\u2193 to navigate, Enter to confirm"),
407
+ teams.map((team, idx) => (React.createElement(Box, { key: team.team.id, marginTop: 1 },
408
+ React.createElement(Text, { color: idx === selectedTeamIndex ? "cyan" : "gray" }, idx === selectedTeamIndex ? "▶ " : " "),
409
+ React.createElement(Text, { bold: idx === selectedTeamIndex }, team.team.name),
410
+ React.createElement(Text, { dimColor: true },
411
+ " (",
412
+ team.role,
413
+ ")"))))),
414
+ React.createElement(Box, { marginTop: 2 },
415
+ React.createElement(Text, { dimColor: true }, "[Enter] Deploy to this team [Esc] Cancel"))));
416
+ }
417
+ if (step === "error") {
418
+ return (React.createElement(Box, { flexDirection: "column" },
419
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
420
+ React.createElement(Text, null, "DEPLOY")),
421
+ React.createElement(Box, { marginTop: 1 },
422
+ React.createElement(Text, { bold: true, color: "red" }, "\u2716 Error")),
423
+ React.createElement(Box, { marginTop: 1 },
424
+ React.createElement(Text, { color: "red" }, errorMsg))));
425
+ }
426
+ if (step === "config") {
427
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
428
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
429
+ React.createElement(Text, null, "DEPLOY")),
430
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
431
+ React.createElement(Text, { bold: true }, "\u250C\u2500 Project Configuration"),
432
+ React.createElement(Box, { marginTop: 1 },
433
+ React.createElement(Text, { color: "cyan" }, " \u2502 Name: "),
434
+ React.createElement(Text, { bold: true }, projectConfig?.name || "Unknown")),
435
+ React.createElement(Box, null,
436
+ React.createElement(Text, { color: "cyan" }, " \u2502 Type: "),
437
+ React.createElement(Text, { bold: true, color: "yellow" }, projectConfig?.framework?.toUpperCase() || "STATIC"),
438
+ needsPod && React.createElement(Text, { color: "magenta" }, " (Onflyt Pod)")),
439
+ React.createElement(Box, null,
440
+ React.createElement(Text, { color: "cyan" }, " \u2502 Deploy: "),
441
+ React.createElement(Text, { color: hasGitRemote ? "green" : "yellow" }, hasGitRemote ? "Git-based" : "Manual (ZIP)")),
442
+ React.createElement(Box, null,
443
+ React.createElement(Text, { color: "cyan" }, " \u2514\u2500 Team: "),
444
+ React.createElement(Text, null, selectedTeam?.team.name))),
445
+ React.createElement(Box, { marginTop: 2, flexDirection: "column" },
446
+ React.createElement(Text, { bold: true }, "\u250C\u2500 Usage Plan"),
447
+ React.createElement(Box, { marginTop: 1 },
448
+ React.createElement(Text, { color: "cyan" }, " \u2502 Plan: "),
449
+ React.createElement(Text, { bold: true, color: "yellow" }, planLabel)),
450
+ React.createElement(Box, null,
451
+ React.createElement(Text, { color: "cyan" }, " \u2502 Credits: "),
452
+ React.createElement(Text, null, currentBalance.balanceFormatted)),
453
+ React.createElement(Box, null,
454
+ React.createElement(Text, { color: "cyan" }, " \u2502 Max Replicas: "),
455
+ React.createElement(Text, null, limits.maxInstancesPerProject)),
456
+ React.createElement(Box, null,
457
+ React.createElement(Text, { color: "cyan" }, " \u2514\u2500 Build Time: "),
458
+ React.createElement(Text, null,
459
+ limits.maxBuildMinutes,
460
+ " min"))),
461
+ needsPod && (React.createElement(Box, { marginTop: 2, flexDirection: "column" },
462
+ React.createElement(Text, { bold: true }, "\u250C\u2500 Onflyt Pod"),
463
+ React.createElement(Box, { marginTop: 1 },
464
+ React.createElement(Text, { color: "cyan" }, " \u2502 Instance: "),
465
+ React.createElement(Text, { color: "yellow" }, selectedInstance?.label || "not set")),
466
+ React.createElement(Box, null,
467
+ React.createElement(Text, { color: "cyan" }, " \u2502 Replicas: "),
468
+ React.createElement(Text, { color: "yellow" }, replicas)),
469
+ React.createElement(Box, null,
470
+ React.createElement(Text, { color: "cyan" }, " \u2514\u2500 Est. Cost: "),
471
+ React.createElement(Text, { color: "yellow" }, estimatedHourlyCost !== null
472
+ ? formatEstimatedCost(estimatedHourlyCost)
473
+ : "Free")))),
474
+ React.createElement(Box, { marginTop: 2 },
475
+ React.createElement(Text, { bold: true, color: "gray" },
476
+ " ",
477
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),
478
+ React.createElement(Box, { marginTop: 2, flexDirection: "column" },
479
+ needsPod && (React.createElement(Box, { marginBottom: 1 },
480
+ React.createElement(Text, { color: "gray" }, " "),
481
+ React.createElement(Text, { bold: true, color: "cyan" }, "[1]"),
482
+ React.createElement(Text, { color: "gray" }, " Configure Onflyt Pod"))),
483
+ React.createElement(Box, null,
484
+ React.createElement(Text, { color: "gray" }, " "),
485
+ React.createElement(Text, { bold: true, color: "green" }, "\u25B8"),
486
+ React.createElement(Text, { color: "gray" }, " "),
487
+ React.createElement(Text, { bold: true, color: "green" }, "[Enter] Deploy now")),
488
+ React.createElement(Box, { marginTop: 1 },
489
+ React.createElement(Text, { color: "gray" }, " "),
490
+ React.createElement(Text, { color: "gray" }, "[q] Quit")))));
491
+ }
492
+ if (step === "instance") {
493
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
494
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
495
+ React.createElement(Text, null, "DEPLOY")),
496
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
497
+ React.createElement(Text, { bold: true }, "\u250C\u2500 Onflyt Pod Configuration"),
498
+ React.createElement(Text, { color: "gray" }, " Choose instance size and replica count"),
499
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, INSTANCE_OPTIONS.map((option, idx) => (React.createElement(Box, { key: option.id },
500
+ React.createElement(Text, { color: "gray" }, " "),
501
+ React.createElement(Text, { bold: selectedInstanceIndex === idx, color: selectedInstanceIndex === idx ? "cyan" : "gray" },
502
+ selectedInstanceIndex === idx ? "▸" : " ",
503
+ "[",
504
+ idx + 1,
505
+ "]",
506
+ " ",
507
+ option.label.padEnd(10)),
508
+ React.createElement(Text, { color: "gray" },
509
+ option.cpu,
510
+ " CPU, ",
511
+ option.ram,
512
+ " RAM, ",
513
+ option.disk,
514
+ " Disk"),
515
+ React.createElement(Text, { color: "yellow" },
516
+ " - ",
517
+ option.hourly))))),
518
+ React.createElement(Box, { marginTop: 1 },
519
+ React.createElement(Text, { color: "cyan" }, " \u2502 Selected: "),
520
+ React.createElement(Text, { bold: true, color: "cyan" }, INSTANCE_OPTIONS[selectedInstanceIndex].label),
521
+ React.createElement(Text, { color: "gray" },
522
+ " ",
523
+ "(",
524
+ INSTANCE_OPTIONS[selectedInstanceIndex].cpu,
525
+ " CPU,",
526
+ " ",
527
+ INSTANCE_OPTIONS[selectedInstanceIndex].ram,
528
+ " RAM)")),
529
+ React.createElement(Box, { marginTop: 1 },
530
+ React.createElement(Text, { color: "cyan" }, " \u2514\u2500 Replicas: "),
531
+ React.createElement(Text, { bold: true, color: "cyan" }, replicas),
532
+ React.createElement(Text, { color: "gray" },
533
+ " ",
534
+ "(max",
535
+ " ",
536
+ Math.min(limits.maxInstancesPerProject, INSTANCE_OPTIONS[selectedInstanceIndex].maxReplicas),
537
+ ")"))),
538
+ React.createElement(Box, { marginTop: 2 },
539
+ React.createElement(Text, { color: "gray" },
540
+ " ",
541
+ "\u2191\u2193 Change instance \u2190\u2192 Change replicas [Enter] Continue [Esc] Back"))));
542
+ }
543
+ if (step === "env") {
544
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
545
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
546
+ React.createElement(Text, null, "DEPLOY")),
547
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
548
+ React.createElement(Text, { bold: true }, "\u250C\u2500 Environment Variables"),
549
+ React.createElement(Text, { color: "gray" }, " Import from .env or add manually"),
550
+ React.createElement(Box, { marginTop: 1 },
551
+ React.createElement(Text, { color: "gray" }, " "),
552
+ React.createElement(Text, { bold: true, color: "cyan" }, "[1]"),
553
+ React.createElement(Text, { color: "gray" }, " Import from .env")),
554
+ React.createElement(Box, null,
555
+ React.createElement(Text, { color: "gray" }, " "),
556
+ React.createElement(Text, { bold: true, color: "cyan" }, "[2]"),
557
+ React.createElement(Text, { color: "gray" }, " Add manually")),
558
+ React.createElement(Box, null,
559
+ React.createElement(Text, { color: "gray" }, " "),
560
+ React.createElement(Text, { bold: true, color: "cyan" }, "[3]"),
561
+ React.createElement(Text, { color: "gray" }, " Skip (use existing)"))),
562
+ React.createElement(Box, { marginTop: 2 },
563
+ React.createElement(Text, { color: "gray" }, " [1/2/3] Select option [Esc] Back")),
564
+ envMessage && (React.createElement(Box, { marginTop: 1 },
565
+ React.createElement(Text, { color: envMessage.includes("✓") ? "green" : "yellow" }, envMessage)))));
566
+ }
567
+ if (step === "deploying") {
568
+ const dots = ".".repeat(dotCount);
569
+ const statusColors = {
570
+ queued: "yellow",
571
+ building: "cyan",
572
+ provisioning: "magenta",
573
+ deployed: "green",
574
+ failed: "red",
575
+ };
576
+ const statusLabels = {
577
+ queued: "QUEUED",
578
+ building: "BUILDING",
579
+ provisioning: "PROVISIONING",
580
+ deployed: "DEPLOYED",
581
+ failed: "FAILED",
582
+ };
583
+ const statusIcons = {
584
+ queued: "⏳",
585
+ building: "🔨",
586
+ provisioning: "⚙️",
587
+ deployed: "✅",
588
+ failed: "❌",
589
+ };
590
+ const progressPercent = uploadProgress.total > 0
591
+ ? Math.round((uploadProgress.uploaded / uploadProgress.total) * 100)
592
+ : 0;
593
+ const getStageInfo = () => {
594
+ switch (deployStage) {
595
+ case "starting":
596
+ return { icon: "⏳", label: "Starting deployment", color: "cyan" };
597
+ case "zipping":
598
+ return { icon: "📦", label: "Creating ZIP archive", color: "cyan" };
599
+ case "uploading":
600
+ if (progressPercent >= 100) {
601
+ return { icon: "✅", label: "Upload completed", color: "green" };
602
+ }
603
+ return {
604
+ icon: "⬆️",
605
+ label: `Uploading ${progressPercent}%`,
606
+ color: "cyan",
607
+ };
608
+ case "deployed":
609
+ return { icon: "✅", label: "Deployed", color: "green" };
610
+ default:
611
+ return { icon: "⏳", label: "Processing", color: "cyan" };
612
+ }
613
+ };
614
+ const stageInfo = getStageInfo();
615
+ const formatBytes = (bytes) => {
616
+ if (bytes === 0)
617
+ return "0 B";
618
+ const k = 1024;
619
+ const sizes = ["B", "KB", "MB", "GB"];
620
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
621
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
622
+ };
623
+ const progressBar = (percent, width = 30) => {
624
+ const filled = Math.round((percent / 100) * width);
625
+ const empty = width - filled;
626
+ return "█".repeat(filled) + "░".repeat(empty);
627
+ };
628
+ const showProgressBar = deployStage === "uploading" && zipSize && progressPercent < 100;
629
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
630
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" },
631
+ React.createElement(Text, null, "DEPLOY")),
632
+ React.createElement(Box, { marginTop: 2 },
633
+ React.createElement(Text, { bold: true, color: stageInfo.color },
634
+ stageInfo.icon,
635
+ " ",
636
+ stageInfo.label),
637
+ React.createElement(Text, { dimColor: true }, dots)),
638
+ deployStage === "zipping" && (React.createElement(Box, { marginTop: 1 },
639
+ React.createElement(Text, { dimColor: true }, "Compressing files..."))),
640
+ showProgressBar && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
641
+ React.createElement(Box, null,
642
+ React.createElement(Text, { dimColor: true },
643
+ "[",
644
+ progressBar(progressPercent),
645
+ "]")),
646
+ React.createElement(Box, null,
647
+ React.createElement(Text, { dimColor: true },
648
+ formatBytes(uploadProgress.uploaded),
649
+ " /",
650
+ " ",
651
+ formatBytes(uploadProgress.total))))),
652
+ React.createElement(Box, { marginTop: 2 },
653
+ React.createElement(Text, { bold: true, color: "cyan" },
654
+ "Status:",
655
+ " "),
656
+ React.createElement(Text, { bold: true, color: statusColors[deploymentStatus] },
657
+ statusIcons[deploymentStatus],
658
+ " ",
659
+ statusLabels[deploymentStatus]),
660
+ React.createElement(Text, { dimColor: true }, dots)),
661
+ deploymentId && (React.createElement(Box, null,
662
+ React.createElement(Text, { bold: true, color: "cyan" }, "ID:"),
663
+ React.createElement(Text, null,
664
+ " ",
665
+ deploymentId))),
666
+ React.createElement(Box, null,
667
+ React.createElement(Text, { bold: true, color: "cyan" }, "Project:"),
668
+ React.createElement(Text, null,
669
+ " ",
670
+ projectConfig?.name)),
671
+ liveLogs.length > 0 && (React.createElement(Box, { marginTop: 2, flexDirection: "column" },
672
+ React.createElement(Text, { bold: true, color: "cyan" }, "Build Logs:"),
673
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, padding: 1, borderStyle: "round", borderDimColor: true }, liveLogs.slice(-15).map((log, idx) => (React.createElement(Text, { key: idx, color: "white" }, log)))))),
674
+ deploymentId && (React.createElement(Box, { marginTop: 2 },
675
+ React.createElement(Text, { color: "gray" },
676
+ "Run 'onflyt tail ",
677
+ deploymentId,
678
+ "' to watch logs")))));
679
+ }
680
+ if (step === "done") {
681
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
682
+ React.createElement(Text, { bold: true, color: "rgb(255,191,0)" }, "DEPLOYED!"),
683
+ React.createElement(Box, { marginTop: 2, flexDirection: "column", borderStyle: "round", borderColor: "green", padding: 1 },
684
+ React.createElement(Text, { bold: true, color: "green" }, "\u2713 Deployment Successful"),
685
+ React.createElement(Box, { marginTop: 1 },
686
+ React.createElement(Text, { bold: true, color: "cyan" }, "ID:"),
687
+ React.createElement(Text, null,
688
+ " ",
689
+ deploymentId)),
690
+ React.createElement(Box, { marginTop: 1 },
691
+ React.createElement(Text, { bold: true, color: "cyan" }, "Live URL:"),
692
+ React.createElement(Text, { bold: true, color: "white" },
693
+ " ",
694
+ liveUrl)),
695
+ previewUrl && (React.createElement(Box, { marginTop: 1 },
696
+ React.createElement(Text, { bold: true, color: "cyan" }, "Preview:"),
697
+ React.createElement(Text, { color: "white" },
698
+ " ",
699
+ previewUrl))),
700
+ needsPod && (React.createElement(Box, { marginTop: 1 },
701
+ React.createElement(Text, { bold: true, color: "magenta" }, "Onflyt Pod:"),
702
+ React.createElement(Text, null,
703
+ " ",
704
+ replicas,
705
+ "x ",
706
+ INSTANCE_OPTIONS[selectedInstanceIndex].label)))),
707
+ React.createElement(Box, { marginTop: 2 },
708
+ React.createElement(Text, { color: "gray" },
709
+ "Run 'onflyt logs ",
710
+ deploymentId,
711
+ "' to view logs"))));
712
+ }
713
+ return null;
714
+ };
715
+ export default Deploy;