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.
- package/dist/App.d.ts +3 -0
- package/dist/App.js +8 -0
- package/dist/commands/credits.d.ts +3 -0
- package/dist/commands/credits.js +101 -0
- package/dist/commands/delete.d.ts +7 -0
- package/dist/commands/delete.js +220 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +715 -0
- package/dist/commands/deployments.d.ts +6 -0
- package/dist/commands/deployments.js +225 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +76 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +422 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.js +150 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +19 -0
- package/dist/commands/logs.d.ts +7 -0
- package/dist/commands/logs.js +307 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +203 -0
- package/dist/commands/rollback.d.ts +6 -0
- package/dist/commands/rollback.js +316 -0
- package/dist/commands/teams.d.ts +3 -0
- package/dist/commands/teams.js +81 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +34 -0
- package/dist/components/Loading.d.ts +13 -0
- package/dist/components/Loading.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +77 -116
- package/dist/lib/api.d.ts +27 -0
- package/dist/lib/api.js +109 -0
- package/dist/lib/config.d.ts +32 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/deploy-api.d.ts +97 -0
- package/dist/lib/deploy-api.js +335 -0
- package/dist/lib/deploy.d.ts +36 -0
- package/dist/lib/deploy.js +181 -0
- package/dist/lib/framework.d.ts +27 -0
- package/dist/lib/framework.js +184 -0
- package/dist/lib/git.d.ts +25 -0
- package/dist/lib/git.js +149 -0
- package/dist/lib/scaffold.d.ts +21 -0
- package/dist/lib/scaffold.js +190 -0
- package/dist/shared/frameworks/registry.d.ts +21 -0
- package/dist/shared/frameworks/registry.js +196 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/limits.d.ts +16 -0
- package/dist/shared/limits.js +44 -0
- package/dist/shared/pricing.d.ts +2 -0
- package/dist/shared/pricing.js +7 -0
- package/dist/shared/templates/registry.d.ts +9 -0
- package/dist/shared/templates/registry.js +47 -0
- package/package.json +2 -3
- package/src/App.tsx +1 -1
- package/src/commands/deploy.tsx +1 -1
- package/src/commands/help.tsx +1 -1
- package/src/commands/init.tsx +1 -1
- package/src/commands/projects.tsx +1 -1
- package/src/components/Loading.tsx +1 -1
- package/src/index.tsx +1 -1
- package/src/lib/deploy-api.ts +3 -3
- package/src/lib/framework.ts +2 -2
- package/src/lib/shared.ts +350 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { api } from "./api.js";
|
|
2
|
+
import { getConfig, API_URL } from "./config.js";
|
|
3
|
+
import { readFileSync, existsSync, unlinkSync, } from "fs";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { TIER_HOURLY_PRICE } from "./shared";
|
|
6
|
+
import { getLimitsBySpend, getTierLabel } from "./shared";
|
|
7
|
+
export const INSTANCE_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
id: "micro",
|
|
10
|
+
label: "Micro",
|
|
11
|
+
cpu: "0.25",
|
|
12
|
+
ram: "1GB",
|
|
13
|
+
disk: "4GB",
|
|
14
|
+
maxReplicas: 1,
|
|
15
|
+
hourly: "Free",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "lite",
|
|
19
|
+
label: "Lite",
|
|
20
|
+
cpu: "0.25",
|
|
21
|
+
ram: "1GB",
|
|
22
|
+
disk: "4GB",
|
|
23
|
+
maxReplicas: 2,
|
|
24
|
+
hourly: formatPrice(TIER_HOURLY_PRICE.lite),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "standard",
|
|
28
|
+
label: "Standard",
|
|
29
|
+
cpu: "0.5",
|
|
30
|
+
ram: "4GB",
|
|
31
|
+
disk: "8GB",
|
|
32
|
+
maxReplicas: 4,
|
|
33
|
+
hourly: formatPrice(TIER_HOURLY_PRICE.standard),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "pro",
|
|
37
|
+
label: "Pro",
|
|
38
|
+
cpu: "1",
|
|
39
|
+
ram: "6GB",
|
|
40
|
+
disk: "12GB",
|
|
41
|
+
maxReplicas: 4,
|
|
42
|
+
hourly: formatPrice(TIER_HOURLY_PRICE.pro),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "business",
|
|
46
|
+
label: "Business",
|
|
47
|
+
cpu: "2",
|
|
48
|
+
ram: "8GB",
|
|
49
|
+
disk: "16GB",
|
|
50
|
+
maxReplicas: 10,
|
|
51
|
+
hourly: formatPrice(TIER_HOURLY_PRICE.business),
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
function formatPrice(price) {
|
|
55
|
+
if (price === 0)
|
|
56
|
+
return "Free";
|
|
57
|
+
return `$${price.toFixed(3)}/hr`;
|
|
58
|
+
}
|
|
59
|
+
import { FRAMEWORKS } from "./shared";
|
|
60
|
+
export function isPodProject(framework) {
|
|
61
|
+
if (!framework)
|
|
62
|
+
return false;
|
|
63
|
+
const config = FRAMEWORKS[framework.toLowerCase()];
|
|
64
|
+
if (!config)
|
|
65
|
+
return false;
|
|
66
|
+
return config.deploymentType === "container";
|
|
67
|
+
}
|
|
68
|
+
export function getDeploymentType(framework) {
|
|
69
|
+
if (!framework)
|
|
70
|
+
return "static";
|
|
71
|
+
const config = FRAMEWORKS[framework.toLowerCase()];
|
|
72
|
+
if (!config)
|
|
73
|
+
return "static";
|
|
74
|
+
return config.deploymentType;
|
|
75
|
+
}
|
|
76
|
+
export function getProjectConfig() {
|
|
77
|
+
try {
|
|
78
|
+
const configData = JSON.parse(readFileSync(`${process.cwd()}/onflyt.json`, "utf-8"));
|
|
79
|
+
return configData;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function loadTeamsWithBalances() {
|
|
86
|
+
const config = getConfig();
|
|
87
|
+
api.setToken(config.token);
|
|
88
|
+
const meData = await api.get("/auth/me");
|
|
89
|
+
const teams = meData.teams || [];
|
|
90
|
+
const balances = {};
|
|
91
|
+
await Promise.all(teams.map(async (team) => {
|
|
92
|
+
try {
|
|
93
|
+
const balanceData = await api.get(`/billing/balance?teamId=${team.team.id}`);
|
|
94
|
+
balances[team.team.id] = balanceData.data || balanceData;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
balances[team.team.id] = {
|
|
98
|
+
balanceUSD: 0,
|
|
99
|
+
balanceFormatted: "$0.00",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
return { teams, balances };
|
|
104
|
+
}
|
|
105
|
+
export async function getTeamDetails(teamId) {
|
|
106
|
+
try {
|
|
107
|
+
const teamData = await api.get(`/teams/${teamId}`);
|
|
108
|
+
const team = teamData.team || teamData;
|
|
109
|
+
return {
|
|
110
|
+
totalLifetimeSpend: team.totalLifetimeSpend || 0,
|
|
111
|
+
balanceUSD: team.balanceUSD || 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { totalLifetimeSpend: 0, balanceUSD: 0 };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function getTeamLimits(totalSpend) {
|
|
119
|
+
return getLimitsBySpend(totalSpend);
|
|
120
|
+
}
|
|
121
|
+
export function getTeamPlanLabel(totalSpend) {
|
|
122
|
+
return getTierLabel(totalSpend);
|
|
123
|
+
}
|
|
124
|
+
export async function findOrCreateProject(teamId, projectName, framework, gitUrl, instanceSize, maxInstances) {
|
|
125
|
+
const projectsRes = await api.get(`/projects/team/${teamId}`);
|
|
126
|
+
const existingProjects = projectsRes.projects || [];
|
|
127
|
+
const existing = existingProjects.find((p) => p.name === projectName);
|
|
128
|
+
if (existing) {
|
|
129
|
+
return {
|
|
130
|
+
id: existing.id,
|
|
131
|
+
name: existing.name,
|
|
132
|
+
isNew: false,
|
|
133
|
+
existingProject: existing,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const createRes = await api.post("/projects", {
|
|
137
|
+
name: projectName,
|
|
138
|
+
framework: framework || "static",
|
|
139
|
+
teamId,
|
|
140
|
+
gitRepoUrl: gitUrl || null,
|
|
141
|
+
instanceSize,
|
|
142
|
+
maxInstances,
|
|
143
|
+
});
|
|
144
|
+
const project = createRes.project || createRes;
|
|
145
|
+
return { id: project.id, name: project.name, isNew: true };
|
|
146
|
+
}
|
|
147
|
+
export async function updateProjectSettings(projectId, instanceSize, maxInstances) {
|
|
148
|
+
const updates = {};
|
|
149
|
+
if (instanceSize)
|
|
150
|
+
updates.instanceSize = instanceSize;
|
|
151
|
+
if (maxInstances)
|
|
152
|
+
updates.maxInstances = maxInstances;
|
|
153
|
+
if (Object.keys(updates).length > 0) {
|
|
154
|
+
await api.patch(`/projects/${projectId}`, updates);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function getProjectDetails(projectId) {
|
|
158
|
+
const res = await api.get(`/projects/${projectId}`);
|
|
159
|
+
return res.project || res;
|
|
160
|
+
}
|
|
161
|
+
export async function startDeployment(projectId, branch, instanceSize, replicas, envVars) {
|
|
162
|
+
const deployRes = await api.post(`/deployments/${projectId}/deploy`, {
|
|
163
|
+
branch: branch || "main",
|
|
164
|
+
instanceSize,
|
|
165
|
+
replicas,
|
|
166
|
+
envVars,
|
|
167
|
+
});
|
|
168
|
+
return deployRes.deploymentId;
|
|
169
|
+
}
|
|
170
|
+
export async function getDeploymentStatus(deploymentId) {
|
|
171
|
+
const res = await api.get(`/deployments/detail/${deploymentId}`);
|
|
172
|
+
const rawStatus = (res.status ||
|
|
173
|
+
res.deployment?.status ||
|
|
174
|
+
"queued").toLowerCase();
|
|
175
|
+
if (rawStatus === "queued" || rawStatus === "uploaded") {
|
|
176
|
+
return { status: "queued" };
|
|
177
|
+
}
|
|
178
|
+
if (rawStatus === "building") {
|
|
179
|
+
return { status: "building" };
|
|
180
|
+
}
|
|
181
|
+
if (rawStatus === "live" || rawStatus === "running") {
|
|
182
|
+
return {
|
|
183
|
+
status: "deployed",
|
|
184
|
+
url: res.deployment?.url || res.deployment?.previewUrl,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (rawStatus === "failed") {
|
|
188
|
+
return { status: "failed" };
|
|
189
|
+
}
|
|
190
|
+
return { status: "queued" };
|
|
191
|
+
}
|
|
192
|
+
export function streamLogs(deploymentId, onLog, onError) {
|
|
193
|
+
let cancelled = false;
|
|
194
|
+
const startStream = async () => {
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(`${API_URL}/deployments/${deploymentId}/logs/stream`, {
|
|
197
|
+
headers: { Authorization: `Bearer ${getConfig().token}` },
|
|
198
|
+
});
|
|
199
|
+
if (!response.body) {
|
|
200
|
+
onError();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const reader = response.body.getReader();
|
|
204
|
+
const decoder = new TextDecoder();
|
|
205
|
+
const read = () => {
|
|
206
|
+
if (cancelled)
|
|
207
|
+
return;
|
|
208
|
+
reader.read().then(({ done, value }) => {
|
|
209
|
+
if (done || cancelled)
|
|
210
|
+
return;
|
|
211
|
+
const chunk = decoder.decode(value);
|
|
212
|
+
const lines = chunk.split("\n").filter((l) => l.startsWith("data:"));
|
|
213
|
+
lines.forEach((l) => {
|
|
214
|
+
const log = l.replace("data: ", "").trim();
|
|
215
|
+
if (log)
|
|
216
|
+
onLog(log);
|
|
217
|
+
});
|
|
218
|
+
read();
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
read();
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
if (!cancelled)
|
|
225
|
+
onError();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
startStream();
|
|
229
|
+
return () => {
|
|
230
|
+
cancelled = true;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export async function getStoredLogs(deploymentId, options) {
|
|
234
|
+
const params = new URLSearchParams();
|
|
235
|
+
if (options?.source)
|
|
236
|
+
params.set("source", options.source);
|
|
237
|
+
if (options?.replicaIndex !== undefined)
|
|
238
|
+
params.set("replicaIndex", String(options.replicaIndex));
|
|
239
|
+
if (options?.limit)
|
|
240
|
+
params.set("limit", String(options.limit));
|
|
241
|
+
if (options?.offset)
|
|
242
|
+
params.set("offset", String(options.offset));
|
|
243
|
+
if (options?.search)
|
|
244
|
+
params.set("search", options.search);
|
|
245
|
+
const queryString = params.toString();
|
|
246
|
+
const endpoint = `/logs/deployments/${deploymentId}/logs${queryString ? `?${queryString}` : ""}`;
|
|
247
|
+
const res = await api.get(endpoint);
|
|
248
|
+
return {
|
|
249
|
+
logs: res.logs || [],
|
|
250
|
+
total: res.total || 0,
|
|
251
|
+
hasMore: res.hasMore || false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
255
|
+
"node_modules",
|
|
256
|
+
".git",
|
|
257
|
+
".gitignore",
|
|
258
|
+
".env",
|
|
259
|
+
".env.local",
|
|
260
|
+
".env.*.local",
|
|
261
|
+
"dist",
|
|
262
|
+
"build",
|
|
263
|
+
".next",
|
|
264
|
+
".output",
|
|
265
|
+
".svelte-kit",
|
|
266
|
+
".turbo",
|
|
267
|
+
".vercel",
|
|
268
|
+
".netlify",
|
|
269
|
+
"coverage",
|
|
270
|
+
".nyc_output",
|
|
271
|
+
"*.log",
|
|
272
|
+
"npm-debug.log*",
|
|
273
|
+
"yarn-debug.log*",
|
|
274
|
+
"yarn-error.log*",
|
|
275
|
+
".DS_Store",
|
|
276
|
+
"Thumbs.db",
|
|
277
|
+
".cache",
|
|
278
|
+
".parcel-cache",
|
|
279
|
+
".vite",
|
|
280
|
+
".nuxt",
|
|
281
|
+
".output",
|
|
282
|
+
"__pycache__",
|
|
283
|
+
"*.pyc",
|
|
284
|
+
".pytest_cache",
|
|
285
|
+
"venv",
|
|
286
|
+
".venv",
|
|
287
|
+
"*.egg-info",
|
|
288
|
+
"vendor",
|
|
289
|
+
".idea",
|
|
290
|
+
".vscode",
|
|
291
|
+
"*.swp",
|
|
292
|
+
"*.swo",
|
|
293
|
+
];
|
|
294
|
+
export function createProjectZip(sourceDir, outputPath, excludePatterns = DEFAULT_EXCLUDE_PATTERNS) {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const excludeStr = excludePatterns.map((p) => `-x "${p}"`).join(" ");
|
|
297
|
+
try {
|
|
298
|
+
execSync(`cd "${sourceDir}" && zip -r "${outputPath}" . -q ${excludeStr}`, { maxBuffer: 50 * 1024 * 1024 });
|
|
299
|
+
resolve(outputPath);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
reject(new Error(`Failed to create zip: ${error.message}`));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
export async function deployManual(projectId, zipPath, framework, instanceSize, replicas, envVars, onUploadProgress, buildSettings) {
|
|
307
|
+
try {
|
|
308
|
+
const uploadRes = await api.uploadFile(`/deployments/${projectId}/upload`, zipPath, "project.zip", onUploadProgress);
|
|
309
|
+
const updates = {};
|
|
310
|
+
if (instanceSize)
|
|
311
|
+
updates.instanceSize = instanceSize;
|
|
312
|
+
if (replicas)
|
|
313
|
+
updates.maxInstances = replicas;
|
|
314
|
+
if (buildSettings?.buildCommand)
|
|
315
|
+
updates.buildCommand = buildSettings.buildCommand;
|
|
316
|
+
if (buildSettings?.outputDirectory)
|
|
317
|
+
updates.outputDirectory = buildSettings.outputDirectory;
|
|
318
|
+
if (buildSettings?.installCommand)
|
|
319
|
+
updates.installCommand = buildSettings.installCommand;
|
|
320
|
+
if (Object.keys(updates).length > 0) {
|
|
321
|
+
await api.patch(`/projects/${projectId}`, updates);
|
|
322
|
+
}
|
|
323
|
+
unlinkSync(zipPath);
|
|
324
|
+
return uploadRes.deploymentId;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (existsSync(zipPath)) {
|
|
328
|
+
try {
|
|
329
|
+
unlinkSync(zipPath);
|
|
330
|
+
}
|
|
331
|
+
catch { }
|
|
332
|
+
}
|
|
333
|
+
throw new Error(`Manual deploy failed: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface DeployProgress {
|
|
2
|
+
stage: "zipping" | "uploading" | "building" | "deployed";
|
|
3
|
+
progress?: number;
|
|
4
|
+
message?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectConfig {
|
|
8
|
+
id?: string;
|
|
9
|
+
name: string;
|
|
10
|
+
teamId: string;
|
|
11
|
+
framework: string;
|
|
12
|
+
buildCommand?: string;
|
|
13
|
+
outputDirectory?: string;
|
|
14
|
+
installCommand?: string;
|
|
15
|
+
startCommand?: string;
|
|
16
|
+
gitRepoUrl?: string;
|
|
17
|
+
gitBranch?: string;
|
|
18
|
+
gitRepoId?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class DeployHelper {
|
|
21
|
+
private cwd;
|
|
22
|
+
private onProgress?;
|
|
23
|
+
constructor(cwd?: string, onProgress?: (p: DeployProgress) => void);
|
|
24
|
+
report(progress: DeployProgress): void;
|
|
25
|
+
createZip(): Promise<Buffer>;
|
|
26
|
+
private gzip;
|
|
27
|
+
uploadZip(zipData: Buffer, projectId: string, apiUrl: string, token: string, onProgress?: (loaded: number, total: number) => void): Promise<{
|
|
28
|
+
deploymentId: string;
|
|
29
|
+
}>;
|
|
30
|
+
private buildMultipartBody;
|
|
31
|
+
pollDeployment(deploymentId: string, apiUrl: string, token: string, onStatus?: (status: string, url?: string) => void): Promise<{
|
|
32
|
+
status: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function validateProjectName(name: string): string | null;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createGzip } from "zlib";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
export class DeployHelper {
|
|
7
|
+
cwd;
|
|
8
|
+
onProgress;
|
|
9
|
+
constructor(cwd = process.cwd(), onProgress) {
|
|
10
|
+
this.cwd = cwd;
|
|
11
|
+
this.onProgress = onProgress;
|
|
12
|
+
}
|
|
13
|
+
report(progress) {
|
|
14
|
+
if (this.onProgress) {
|
|
15
|
+
this.onProgress(progress);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async createZip() {
|
|
19
|
+
this.report({
|
|
20
|
+
stage: "zipping",
|
|
21
|
+
progress: 0,
|
|
22
|
+
message: "Creating zip archive...",
|
|
23
|
+
});
|
|
24
|
+
const files = [];
|
|
25
|
+
const ignoreDirs = [
|
|
26
|
+
"node_modules",
|
|
27
|
+
".git",
|
|
28
|
+
".next",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
".output",
|
|
32
|
+
".onflyt",
|
|
33
|
+
".venv",
|
|
34
|
+
"venv",
|
|
35
|
+
"__pycache__",
|
|
36
|
+
".cache",
|
|
37
|
+
".turbo",
|
|
38
|
+
"coverage",
|
|
39
|
+
".nyc_output",
|
|
40
|
+
"target",
|
|
41
|
+
];
|
|
42
|
+
const ignoreFiles = [
|
|
43
|
+
".DS_Store",
|
|
44
|
+
".gitignore",
|
|
45
|
+
".env",
|
|
46
|
+
".env.local",
|
|
47
|
+
".env.production",
|
|
48
|
+
"*.log",
|
|
49
|
+
"npm-debug.log*",
|
|
50
|
+
"yarn-debug.log*",
|
|
51
|
+
"yarn-error.log*",
|
|
52
|
+
];
|
|
53
|
+
const walkDir = (dir, basePath = "") => {
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
return;
|
|
56
|
+
const items = readdirSync(dir, { withFileTypes: true });
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
const itemPath = join(dir, item.name);
|
|
59
|
+
const relativePath = basePath ? `${basePath}/${item.name}` : item.name;
|
|
60
|
+
if (item.isDirectory()) {
|
|
61
|
+
if (!ignoreDirs.includes(item.name) && !item.name.startsWith(".")) {
|
|
62
|
+
walkDir(itemPath, relativePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const shouldIgnore = ignoreFiles.some((pattern) => {
|
|
67
|
+
if (pattern.startsWith("*")) {
|
|
68
|
+
return item.name.endsWith(pattern.slice(1));
|
|
69
|
+
}
|
|
70
|
+
return item.name === pattern;
|
|
71
|
+
});
|
|
72
|
+
if (!shouldIgnore) {
|
|
73
|
+
try {
|
|
74
|
+
const data = readFileSync(itemPath);
|
|
75
|
+
files.push({ path: relativePath, data });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// skip files we can't read
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
walkDir(this.cwd);
|
|
85
|
+
let content = "";
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
content += `${file.path}:${file.data.toString("base64")}\n`;
|
|
88
|
+
}
|
|
89
|
+
const zipBuffer = await this.gzip(Buffer.from(content, "utf-8"));
|
|
90
|
+
this.report({
|
|
91
|
+
stage: "zipping",
|
|
92
|
+
progress: 100,
|
|
93
|
+
message: `Zipped ${files.length} files`,
|
|
94
|
+
});
|
|
95
|
+
return zipBuffer;
|
|
96
|
+
}
|
|
97
|
+
gzip(data) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const chunks = [];
|
|
100
|
+
const gzip = createGzip();
|
|
101
|
+
gzip.on("data", (chunk) => chunks.push(chunk));
|
|
102
|
+
gzip.on("end", () => resolve(Buffer.concat(chunks)));
|
|
103
|
+
gzip.on("error", reject);
|
|
104
|
+
Readable.from(data).pipe(gzip);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async uploadZip(zipData, projectId, apiUrl, token, onProgress) {
|
|
108
|
+
this.report({
|
|
109
|
+
stage: "uploading",
|
|
110
|
+
progress: 0,
|
|
111
|
+
message: "Starting upload...",
|
|
112
|
+
});
|
|
113
|
+
const boundary = `----FormBoundary${createHash("md5").update(Date.now().toString()).digest("hex")}`;
|
|
114
|
+
const body = this.buildMultipartBody(zipData, boundary);
|
|
115
|
+
const totalSize = body.length;
|
|
116
|
+
const response = await fetch(`${apiUrl}/deployments/${projectId}/upload`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
120
|
+
Authorization: `Bearer ${token}`,
|
|
121
|
+
},
|
|
122
|
+
body: new Uint8Array(body),
|
|
123
|
+
});
|
|
124
|
+
const result = await response.json();
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
throw new Error(result.error || "Upload failed");
|
|
127
|
+
}
|
|
128
|
+
this.report({
|
|
129
|
+
stage: "uploading",
|
|
130
|
+
progress: 100,
|
|
131
|
+
message: "Upload complete",
|
|
132
|
+
});
|
|
133
|
+
return { deploymentId: result.deploymentId };
|
|
134
|
+
}
|
|
135
|
+
buildMultipartBody(data, boundary) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
const header = Buffer.from(`--${boundary}\r\nContent-Type: application/octet-stream\r\nContent-Disposition: form-data; name="file"; filename="source.zip"\r\n\r\n`, "utf-8");
|
|
138
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`, "utf-8");
|
|
139
|
+
parts.push(header);
|
|
140
|
+
parts.push(data);
|
|
141
|
+
parts.push(footer);
|
|
142
|
+
return Buffer.concat(parts);
|
|
143
|
+
}
|
|
144
|
+
async pollDeployment(deploymentId, apiUrl, token, onStatus) {
|
|
145
|
+
const maxAttempts = 60;
|
|
146
|
+
let attempts = 0;
|
|
147
|
+
while (attempts < maxAttempts) {
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
149
|
+
const response = await fetch(`${apiUrl}/deployments/${deploymentId}`, {
|
|
150
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
151
|
+
});
|
|
152
|
+
const result = await response.json();
|
|
153
|
+
if (result.deployment) {
|
|
154
|
+
const status = result.deployment.status;
|
|
155
|
+
const url = result.deployment.url;
|
|
156
|
+
if (onStatus) {
|
|
157
|
+
onStatus(status, url);
|
|
158
|
+
}
|
|
159
|
+
if (status === "active" ||
|
|
160
|
+
status === "deployed" ||
|
|
161
|
+
status === "failed") {
|
|
162
|
+
return { status, url };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
attempts++;
|
|
166
|
+
}
|
|
167
|
+
return { status: "timeout" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function validateProjectName(name) {
|
|
171
|
+
if (!name || name.length < 3) {
|
|
172
|
+
return "Name must be at least 3 characters";
|
|
173
|
+
}
|
|
174
|
+
if (name.length > 30) {
|
|
175
|
+
return "Name must be less than 30 characters";
|
|
176
|
+
}
|
|
177
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
178
|
+
return "Name must be lowercase alphanumeric with dashes only";
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Framework {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
buildCommand?: string;
|
|
5
|
+
outputDirectory?: string;
|
|
6
|
+
installCommand?: string;
|
|
7
|
+
startCommand?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const FRAMEWORKS: any;
|
|
10
|
+
export declare const FRAMEWORK_LIST: {
|
|
11
|
+
id: string;
|
|
12
|
+
name: any;
|
|
13
|
+
family: any;
|
|
14
|
+
isServer: any;
|
|
15
|
+
}[];
|
|
16
|
+
export declare class FrameworkDetector {
|
|
17
|
+
private cwd;
|
|
18
|
+
constructor(cwd?: string);
|
|
19
|
+
detect(): Framework | null;
|
|
20
|
+
detectOutputDirectory(frameworkId: string): string | null;
|
|
21
|
+
private toFramework;
|
|
22
|
+
private hasFile;
|
|
23
|
+
private readFile;
|
|
24
|
+
private readPackageJson;
|
|
25
|
+
getSuggestions(): Framework[];
|
|
26
|
+
}
|
|
27
|
+
export { getFramework } from "./shared";
|