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/dist/index.js +13093 -0
- package/package.json +61 -0
- 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 +74 -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 +68 -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 +515 -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/shared.ts +353 -0
- package/src/types.d.ts +5 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
3
|
+
import { join, basename } from "path";
|
|
4
|
+
import { pipeline } from "stream/promises";
|
|
5
|
+
import { createGzip } from "zlib";
|
|
6
|
+
import { Readable } from "stream";
|
|
7
|
+
|
|
8
|
+
export interface DeployProgress {
|
|
9
|
+
stage: "zipping" | "uploading" | "building" | "deployed";
|
|
10
|
+
progress?: number;
|
|
11
|
+
message?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProjectConfig {
|
|
16
|
+
id?: string;
|
|
17
|
+
name: string;
|
|
18
|
+
teamId: string;
|
|
19
|
+
framework: string;
|
|
20
|
+
buildCommand?: string;
|
|
21
|
+
outputDirectory?: string;
|
|
22
|
+
installCommand?: string;
|
|
23
|
+
startCommand?: string;
|
|
24
|
+
gitRepoUrl?: string;
|
|
25
|
+
gitBranch?: string;
|
|
26
|
+
gitRepoId?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DeployHelper {
|
|
30
|
+
private cwd: string;
|
|
31
|
+
private onProgress?: (progress: DeployProgress) => void;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
cwd: string = process.cwd(),
|
|
35
|
+
onProgress?: (p: DeployProgress) => void,
|
|
36
|
+
) {
|
|
37
|
+
this.cwd = cwd;
|
|
38
|
+
this.onProgress = onProgress;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
report(progress: DeployProgress) {
|
|
42
|
+
if (this.onProgress) {
|
|
43
|
+
this.onProgress(progress);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async createZip(): Promise<Buffer> {
|
|
48
|
+
this.report({
|
|
49
|
+
stage: "zipping",
|
|
50
|
+
progress: 0,
|
|
51
|
+
message: "Creating zip archive...",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const files: { path: string; data: Buffer }[] = [];
|
|
55
|
+
const ignoreDirs = [
|
|
56
|
+
"node_modules",
|
|
57
|
+
".git",
|
|
58
|
+
".next",
|
|
59
|
+
"dist",
|
|
60
|
+
"build",
|
|
61
|
+
".output",
|
|
62
|
+
".onflyt",
|
|
63
|
+
".venv",
|
|
64
|
+
"venv",
|
|
65
|
+
"__pycache__",
|
|
66
|
+
".cache",
|
|
67
|
+
".turbo",
|
|
68
|
+
"coverage",
|
|
69
|
+
".nyc_output",
|
|
70
|
+
"target",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const ignoreFiles = [
|
|
74
|
+
".DS_Store",
|
|
75
|
+
".gitignore",
|
|
76
|
+
".env",
|
|
77
|
+
".env.local",
|
|
78
|
+
".env.production",
|
|
79
|
+
"*.log",
|
|
80
|
+
"npm-debug.log*",
|
|
81
|
+
"yarn-debug.log*",
|
|
82
|
+
"yarn-error.log*",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const walkDir = (dir: string, basePath: string = "") => {
|
|
86
|
+
if (!existsSync(dir)) return;
|
|
87
|
+
|
|
88
|
+
const items = readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const itemPath = join(dir, item.name);
|
|
92
|
+
const relativePath = basePath ? `${basePath}/${item.name}` : item.name;
|
|
93
|
+
|
|
94
|
+
if (item.isDirectory()) {
|
|
95
|
+
if (!ignoreDirs.includes(item.name) && !item.name.startsWith(".")) {
|
|
96
|
+
walkDir(itemPath, relativePath);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
const shouldIgnore = ignoreFiles.some((pattern) => {
|
|
100
|
+
if (pattern.startsWith("*")) {
|
|
101
|
+
return item.name.endsWith(pattern.slice(1));
|
|
102
|
+
}
|
|
103
|
+
return item.name === pattern;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!shouldIgnore) {
|
|
107
|
+
try {
|
|
108
|
+
const data = readFileSync(itemPath);
|
|
109
|
+
files.push({ path: relativePath, data });
|
|
110
|
+
} catch {
|
|
111
|
+
// skip files we can't read
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
walkDir(this.cwd);
|
|
119
|
+
|
|
120
|
+
let content = "";
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
content += `${file.path}:${file.data.toString("base64")}\n`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const zipBuffer = await this.gzip(Buffer.from(content, "utf-8"));
|
|
126
|
+
|
|
127
|
+
this.report({
|
|
128
|
+
stage: "zipping",
|
|
129
|
+
progress: 100,
|
|
130
|
+
message: `Zipped ${files.length} files`,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return zipBuffer;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private gzip(data: Buffer): Promise<Buffer> {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const chunks: Buffer[] = [];
|
|
139
|
+
const gzip = createGzip();
|
|
140
|
+
|
|
141
|
+
gzip.on("data", (chunk) => chunks.push(chunk));
|
|
142
|
+
gzip.on("end", () => resolve(Buffer.concat(chunks)));
|
|
143
|
+
gzip.on("error", reject);
|
|
144
|
+
|
|
145
|
+
Readable.from(data).pipe(gzip);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async uploadZip(
|
|
150
|
+
zipData: Buffer,
|
|
151
|
+
projectId: string,
|
|
152
|
+
apiUrl: string,
|
|
153
|
+
token: string,
|
|
154
|
+
onProgress?: (loaded: number, total: number) => void,
|
|
155
|
+
): Promise<{ deploymentId: string }> {
|
|
156
|
+
this.report({
|
|
157
|
+
stage: "uploading",
|
|
158
|
+
progress: 0,
|
|
159
|
+
message: "Starting upload...",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const boundary = `----FormBoundary${createHash("md5").update(Date.now().toString()).digest("hex")}`;
|
|
163
|
+
|
|
164
|
+
const body = this.buildMultipartBody(zipData, boundary);
|
|
165
|
+
const totalSize = body.length;
|
|
166
|
+
|
|
167
|
+
const response = await fetch(`${apiUrl}/deployments/${projectId}/upload`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
171
|
+
Authorization: `Bearer ${token}`,
|
|
172
|
+
},
|
|
173
|
+
body: new Uint8Array(body),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await response.json();
|
|
177
|
+
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
throw new Error(result.error || "Upload failed");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.report({
|
|
183
|
+
stage: "uploading",
|
|
184
|
+
progress: 100,
|
|
185
|
+
message: "Upload complete",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { deploymentId: result.deploymentId };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private buildMultipartBody(data: Buffer, boundary: string): Buffer {
|
|
192
|
+
const parts: Buffer[] = [];
|
|
193
|
+
|
|
194
|
+
const header = Buffer.from(
|
|
195
|
+
`--${boundary}\r\nContent-Type: application/octet-stream\r\nContent-Disposition: form-data; name="file"; filename="source.zip"\r\n\r\n`,
|
|
196
|
+
"utf-8",
|
|
197
|
+
);
|
|
198
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`, "utf-8");
|
|
199
|
+
|
|
200
|
+
parts.push(header);
|
|
201
|
+
parts.push(data);
|
|
202
|
+
parts.push(footer);
|
|
203
|
+
|
|
204
|
+
return Buffer.concat(parts);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async pollDeployment(
|
|
208
|
+
deploymentId: string,
|
|
209
|
+
apiUrl: string,
|
|
210
|
+
token: string,
|
|
211
|
+
onStatus?: (status: string, url?: string) => void,
|
|
212
|
+
): Promise<{ status: string; url?: string }> {
|
|
213
|
+
const maxAttempts = 60;
|
|
214
|
+
let attempts = 0;
|
|
215
|
+
|
|
216
|
+
while (attempts < maxAttempts) {
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
218
|
+
|
|
219
|
+
const response = await fetch(`${apiUrl}/deployments/${deploymentId}`, {
|
|
220
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await response.json();
|
|
224
|
+
|
|
225
|
+
if (result.deployment) {
|
|
226
|
+
const status = result.deployment.status;
|
|
227
|
+
const url = result.deployment.url;
|
|
228
|
+
|
|
229
|
+
if (onStatus) {
|
|
230
|
+
onStatus(status, url);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
status === "active" ||
|
|
235
|
+
status === "deployed" ||
|
|
236
|
+
status === "failed"
|
|
237
|
+
) {
|
|
238
|
+
return { status, url };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
attempts++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { status: "timeout" };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function validateProjectName(name: string): string | null {
|
|
250
|
+
if (!name || name.length < 3) {
|
|
251
|
+
return "Name must be at least 3 characters";
|
|
252
|
+
}
|
|
253
|
+
if (name.length > 30) {
|
|
254
|
+
return "Name must be less than 30 characters";
|
|
255
|
+
}
|
|
256
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
257
|
+
return "Name must be lowercase alphanumeric with dashes only";
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import {
|
|
4
|
+
FRAMEWORKS as SHARED_FRAMEWORKS,
|
|
5
|
+
getFramework,
|
|
6
|
+
getDefaultOutputDirectory,
|
|
7
|
+
FrameworkConfig,
|
|
8
|
+
} from "../shared";
|
|
9
|
+
|
|
10
|
+
export interface Framework {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
buildCommand?: string;
|
|
14
|
+
outputDirectory?: string;
|
|
15
|
+
installCommand?: string;
|
|
16
|
+
startCommand?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const FRAMEWORKS = SHARED_FRAMEWORKS;
|
|
20
|
+
|
|
21
|
+
export const FRAMEWORK_LIST = Object.entries(SHARED_FRAMEWORKS).map(
|
|
22
|
+
([id, config]) => ({
|
|
23
|
+
id,
|
|
24
|
+
name: config.label,
|
|
25
|
+
family: config.family,
|
|
26
|
+
isServer: config.isServer,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export class FrameworkDetector {
|
|
31
|
+
private cwd: string;
|
|
32
|
+
|
|
33
|
+
constructor(cwd: string = process.cwd()) {
|
|
34
|
+
this.cwd = cwd;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
detect(): Framework | null {
|
|
38
|
+
if (
|
|
39
|
+
this.hasFile("next.config.js") ||
|
|
40
|
+
this.hasFile("next.config.mjs") ||
|
|
41
|
+
this.hasFile("next.config.ts")
|
|
42
|
+
) {
|
|
43
|
+
return this.toFramework("nextjs");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.hasFile("nuxt.config.ts") || this.hasFile("nuxt.config.js")) {
|
|
47
|
+
return this.toFramework("nuxt");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.hasFile("package.json")) {
|
|
51
|
+
const pkg = this.readPackageJson();
|
|
52
|
+
if (!pkg) return null;
|
|
53
|
+
|
|
54
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) {
|
|
55
|
+
return this.toFramework("nextjs");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) {
|
|
59
|
+
if (pkg.dependencies?.remix) return this.toFramework("remix");
|
|
60
|
+
if (pkg.dependencies?.nuxt || pkg.devDependencies?.nuxt)
|
|
61
|
+
return this.toFramework("nuxt");
|
|
62
|
+
if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte)
|
|
63
|
+
return this.toFramework("svelte");
|
|
64
|
+
if (pkg.dependencies?.angular || pkg.devDependencies?.angular)
|
|
65
|
+
return this.toFramework("angular");
|
|
66
|
+
if (pkg.dependencies?.hono || pkg.devDependencies?.hono)
|
|
67
|
+
return this.toFramework("hono");
|
|
68
|
+
if (pkg.dependencies?.nestjs || pkg.devDependencies?.nestjs)
|
|
69
|
+
return this.toFramework("nestjs");
|
|
70
|
+
return this.toFramework("react");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (pkg.dependencies?.vue || pkg.devDependencies?.vue) {
|
|
74
|
+
return this.toFramework("vue");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (pkg.dependencies?.express || pkg.devDependencies?.express) {
|
|
78
|
+
return this.toFramework("express");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pkg.dependencies?.fastapi || pkg.devDependencies?.fastapi) {
|
|
82
|
+
return this.toFramework("fastapi");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pkg.dependencies?.django || pkg.devDependencies?.django) {
|
|
86
|
+
return this.toFramework("django");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pkg.dependencies?.flask || pkg.devDependencies?.flask) {
|
|
90
|
+
return this.toFramework("flask");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (pkg.dependencies?.hono || pkg.devDependencies?.hono) {
|
|
94
|
+
return this.toFramework("hono");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pkg.dependencies?.nestjs || pkg.devDependencies?.nestjs) {
|
|
98
|
+
return this.toFramework("nestjs");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pkg.scripts?.dev || pkg.scripts?.start) {
|
|
102
|
+
return this.toFramework("node");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.hasFile("requirements.txt") || this.hasFile("Pipfile")) {
|
|
107
|
+
const content =
|
|
108
|
+
this.readFile("requirements.txt") || this.readFile("Pipfile") || "";
|
|
109
|
+
|
|
110
|
+
if (content.includes("fastapi")) return this.toFramework("fastapi");
|
|
111
|
+
if (content.includes("django")) return this.toFramework("django");
|
|
112
|
+
if (content.includes("flask")) return this.toFramework("flask");
|
|
113
|
+
|
|
114
|
+
return this.toFramework("python");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.hasFile("go.mod")) {
|
|
118
|
+
return this.toFramework("go");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.hasFile("Cargo.toml")) {
|
|
122
|
+
return this.toFramework("rust");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.hasFile("index.html")) {
|
|
126
|
+
return this.toFramework("static");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.hasFile("Dockerfile")) {
|
|
130
|
+
return this.toFramework("docker");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
detectOutputDirectory(frameworkId: string): string | null {
|
|
137
|
+
const lowerId = frameworkId.toLowerCase();
|
|
138
|
+
|
|
139
|
+
if (this.hasFile("package.json")) {
|
|
140
|
+
const pkg = this.readPackageJson();
|
|
141
|
+
if (pkg) {
|
|
142
|
+
const scripts = pkg.scripts || {};
|
|
143
|
+
if (scripts.build) {
|
|
144
|
+
if (lowerId === "nextjs") {
|
|
145
|
+
if (this.hasFile(".next")) return ".next";
|
|
146
|
+
}
|
|
147
|
+
if (lowerId === "react" || lowerId === "vue" || lowerId === "vite") {
|
|
148
|
+
if (this.hasFile("dist")) return "dist";
|
|
149
|
+
if (this.hasFile("build")) return "build";
|
|
150
|
+
}
|
|
151
|
+
if (lowerId === "nuxt") {
|
|
152
|
+
if (this.hasFile(".output")) return ".output";
|
|
153
|
+
}
|
|
154
|
+
if (lowerId === "svelte") {
|
|
155
|
+
if (this.hasFile("build")) return "build";
|
|
156
|
+
if (this.hasFile("dist")) return "dist";
|
|
157
|
+
}
|
|
158
|
+
if (lowerId === "astro") {
|
|
159
|
+
if (this.hasFile("dist")) return "dist";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return getDefaultOutputDirectory(lowerId) || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private toFramework(id: string): Framework {
|
|
169
|
+
const config = SHARED_FRAMEWORKS[id.toLowerCase()];
|
|
170
|
+
if (!config) {
|
|
171
|
+
return { id, name: id };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
id,
|
|
175
|
+
name: config.label,
|
|
176
|
+
buildCommand: config.defaults.buildCommand,
|
|
177
|
+
outputDirectory: config.defaults.outputDirectory,
|
|
178
|
+
installCommand: config.defaults.installCommand,
|
|
179
|
+
startCommand: config.defaults.startCommand,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private hasFile(filename: string): boolean {
|
|
184
|
+
return existsSync(join(this.cwd, filename));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private readFile(filename: string): string | null {
|
|
188
|
+
try {
|
|
189
|
+
const path = join(this.cwd, filename);
|
|
190
|
+
if (existsSync(path)) {
|
|
191
|
+
return readFileSync(path, "utf-8");
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// ignore
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private readPackageJson(): {
|
|
200
|
+
dependencies?: Record<string, string>;
|
|
201
|
+
devDependencies?: Record<string, string>;
|
|
202
|
+
scripts?: Record<string, string>;
|
|
203
|
+
} | null {
|
|
204
|
+
try {
|
|
205
|
+
const content = this.readFile("package.json");
|
|
206
|
+
if (content) {
|
|
207
|
+
return JSON.parse(content);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// ignore
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getSuggestions(): Framework[] {
|
|
216
|
+
return Object.entries(SHARED_FRAMEWORKS).map(([id, config]) => ({
|
|
217
|
+
id,
|
|
218
|
+
name: config.label,
|
|
219
|
+
buildCommand: config.defaults.buildCommand,
|
|
220
|
+
outputDirectory: config.defaults.outputDirectory,
|
|
221
|
+
installCommand: config.defaults.installCommand,
|
|
222
|
+
startCommand: config.defaults.startCommand,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export { getFramework } from "../shared";
|
package/src/lib/git.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export interface GitRemote {
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
type: "github" | "gitlab" | "other";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GitInfo {
|
|
12
|
+
isGitRepo: boolean;
|
|
13
|
+
remotes: GitRemote[];
|
|
14
|
+
currentBranch: string;
|
|
15
|
+
rootDir: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class GitDetector {
|
|
19
|
+
private cwd: string;
|
|
20
|
+
|
|
21
|
+
constructor(cwd: string = process.cwd()) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async detect(): Promise<GitInfo> {
|
|
26
|
+
try {
|
|
27
|
+
const isGitRepo = this.isGitRepository();
|
|
28
|
+
if (!isGitRepo) {
|
|
29
|
+
return {
|
|
30
|
+
isGitRepo: false,
|
|
31
|
+
remotes: [],
|
|
32
|
+
currentBranch: "",
|
|
33
|
+
rootDir: this.cwd,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rootDir = this.getRootDir();
|
|
38
|
+
const remotes = this.getRemotes();
|
|
39
|
+
const currentBranch = this.getCurrentBranch();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
isGitRepo: true,
|
|
43
|
+
remotes,
|
|
44
|
+
currentBranch,
|
|
45
|
+
rootDir,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return {
|
|
49
|
+
isGitRepo: false,
|
|
50
|
+
remotes: [],
|
|
51
|
+
currentBranch: "",
|
|
52
|
+
rootDir: this.cwd,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private isGitRepository(): boolean {
|
|
58
|
+
try {
|
|
59
|
+
execSync("git rev-parse --git-dir", {
|
|
60
|
+
cwd: this.cwd,
|
|
61
|
+
stdio: "ignore",
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getRootDir(): string {
|
|
70
|
+
try {
|
|
71
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
72
|
+
cwd: this.cwd,
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
}).trim();
|
|
75
|
+
return root;
|
|
76
|
+
} catch {
|
|
77
|
+
return this.cwd;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getCurrentBranch(): string {
|
|
82
|
+
try {
|
|
83
|
+
const branch = execSync("git branch --show-current", {
|
|
84
|
+
cwd: this.cwd,
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
}).trim();
|
|
87
|
+
return branch;
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private getRemotes(): GitRemote[] {
|
|
94
|
+
try {
|
|
95
|
+
const output = execSync("git remote -v", {
|
|
96
|
+
cwd: this.cwd,
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const remotes: GitRemote[] = [];
|
|
101
|
+
const lines = output.trim().split("\n");
|
|
102
|
+
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
|
|
105
|
+
if (match) {
|
|
106
|
+
const [, name, url] = match;
|
|
107
|
+
const type = this.detectRemoteType(url);
|
|
108
|
+
if (!remotes.find((r) => r.name === name)) {
|
|
109
|
+
remotes.push({ name, url, type });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return remotes;
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private detectRemoteType(url: string): "github" | "gitlab" | "other" {
|
|
121
|
+
if (url.includes("github.com")) return "github";
|
|
122
|
+
if (url.includes("gitlab.com")) return "gitlab";
|
|
123
|
+
return "other";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getBranches(): Promise<string[]> {
|
|
127
|
+
try {
|
|
128
|
+
const output = execSync("git branch -a", {
|
|
129
|
+
cwd: this.cwd,
|
|
130
|
+
encoding: "utf-8",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return output
|
|
134
|
+
.trim()
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((b) => b.replace(/^\*?\s*/, "").trim())
|
|
137
|
+
.filter(
|
|
138
|
+
(b) =>
|
|
139
|
+
(b && !b.startsWith("remotes/")) || b.startsWith("remotes/origin/"),
|
|
140
|
+
)
|
|
141
|
+
.map((b) => b.replace(/^remotes\/origin\//, ""));
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getRepoId(): Promise<number | null> {
|
|
148
|
+
try {
|
|
149
|
+
const output = execSync("git remote get-url origin", {
|
|
150
|
+
cwd: this.cwd,
|
|
151
|
+
encoding: "utf-8",
|
|
152
|
+
}).trim();
|
|
153
|
+
|
|
154
|
+
const match = output.match(/git@github\.com:(\d+)\//);
|
|
155
|
+
if (match) {
|
|
156
|
+
return parseInt(match[1], 10);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const urlMatch = output.match(/github\.com\/[^\/]+\/([^\/\.]+)/);
|
|
160
|
+
if (urlMatch) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static getProjectNameFromRepo(repoUrl: string): string {
|
|
171
|
+
const match = repoUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
172
|
+
return match ? match[1] : "";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static getOwnerFromRepo(repoUrl: string): string {
|
|
176
|
+
const match = repoUrl.match(/github\.com\/([^\/]+)\//);
|
|
177
|
+
return match ? match[1] : "";
|
|
178
|
+
}
|
|
179
|
+
}
|