gitlab-mcp 1.2.1 → 1.3.0
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/README.md +30 -27
- package/dist/config/dotenv.js +6 -2
- package/dist/config/dotenv.js.map +1 -1
- package/dist/config/env.d.ts +3 -1
- package/dist/config/env.js +15 -1
- package/dist/config/env.js.map +1 -1
- package/dist/http-app.js +10 -3
- package/dist/http-app.js.map +1 -1
- package/dist/http.js +5 -4
- package/dist/http.js.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-context.d.ts +2 -1
- package/dist/lib/auth-context.js.map +1 -1
- package/dist/lib/gitlab-client.d.ts +42 -8
- package/dist/lib/gitlab-client.js +380 -42
- package/dist/lib/gitlab-client.js.map +1 -1
- package/dist/lib/network.js +12 -6
- package/dist/lib/network.js.map +1 -1
- package/dist/lib/oauth-scopes.d.ts +2 -0
- package/dist/lib/oauth-scopes.js +16 -0
- package/dist/lib/oauth-scopes.js.map +1 -0
- package/dist/lib/regex.d.ts +5 -0
- package/dist/lib/regex.js +111 -0
- package/dist/lib/regex.js.map +1 -0
- package/dist/lib/request-runtime.js +24 -11
- package/dist/lib/request-runtime.js.map +1 -1
- package/dist/tools/gitlab.js +193 -3
- package/dist/tools/gitlab.js.map +1 -1
- package/dist/types/auth.d.ts +1 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/context.d.ts +1 -0
- package/docs/architecture.md +1 -1
- package/docs/authentication.md +4 -1
- package/docs/configuration.md +23 -21
- package/docs/deployment.md +2 -0
- package/docs/mcp-integration-testing-best-practices.md +381 -730
- package/docs/tools.md +24 -14
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
1
3
|
import * as fs from "node:fs/promises";
|
|
2
4
|
import * as path from "node:path";
|
|
3
5
|
import { getSessionAuth } from "./auth-context.js";
|
|
@@ -13,6 +15,7 @@ export class GitLabApiError extends Error {
|
|
|
13
15
|
}
|
|
14
16
|
export class GitLabClient {
|
|
15
17
|
static DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
18
|
+
static DEFAULT_MAX_LOCAL_FILE_BYTES = 250 * 1024 * 1024;
|
|
16
19
|
static DEFAULT_MAX_RESPONSE_BODY_BYTES = 25 * 1024 * 1024;
|
|
17
20
|
baseApiUrl;
|
|
18
21
|
apiUrls;
|
|
@@ -20,6 +23,7 @@ export class GitLabClient {
|
|
|
20
23
|
defaultToken;
|
|
21
24
|
timeoutMs;
|
|
22
25
|
maxAttachmentBytes;
|
|
26
|
+
maxLocalFileBytes;
|
|
23
27
|
maxResponseBodyBytes;
|
|
24
28
|
beforeRequest;
|
|
25
29
|
constructor(baseApiUrl, defaultToken, options = {}) {
|
|
@@ -32,6 +36,7 @@ export class GitLabClient {
|
|
|
32
36
|
this.timeoutMs = options.timeoutMs ?? 20_000;
|
|
33
37
|
this.maxAttachmentBytes =
|
|
34
38
|
options.maxAttachmentBytes ?? GitLabClient.DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
39
|
+
this.maxLocalFileBytes = options.maxLocalFileBytes ?? GitLabClient.DEFAULT_MAX_LOCAL_FILE_BYTES;
|
|
35
40
|
this.maxResponseBodyBytes =
|
|
36
41
|
options.maxResponseBodyBytes ?? GitLabClient.DEFAULT_MAX_RESPONSE_BODY_BYTES;
|
|
37
42
|
this.beforeRequest = options.beforeRequest;
|
|
@@ -229,6 +234,9 @@ export class GitLabClient {
|
|
|
229
234
|
getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
|
|
230
235
|
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`, options);
|
|
231
236
|
}
|
|
237
|
+
getMergeRequestConflicts(projectId, mergeRequestIid, options = {}) {
|
|
238
|
+
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/conflicts`, options);
|
|
239
|
+
}
|
|
232
240
|
listMergeRequestDiscussions(projectId, mergeRequestIid, options = {}) {
|
|
233
241
|
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions`, options);
|
|
234
242
|
}
|
|
@@ -501,6 +509,18 @@ export class GitLabClient {
|
|
|
501
509
|
getPipeline(projectId, pipelineId, options = {}) {
|
|
502
510
|
return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}`, options);
|
|
503
511
|
}
|
|
512
|
+
listDeployments(projectId, options = {}) {
|
|
513
|
+
return this.get(`/projects/${encode(projectId)}/deployments`, options);
|
|
514
|
+
}
|
|
515
|
+
getDeployment(projectId, deploymentId, options = {}) {
|
|
516
|
+
return this.get(`/projects/${encode(projectId)}/deployments/${encode(deploymentId)}`, options);
|
|
517
|
+
}
|
|
518
|
+
listEnvironments(projectId, options = {}) {
|
|
519
|
+
return this.get(`/projects/${encode(projectId)}/environments`, options);
|
|
520
|
+
}
|
|
521
|
+
getEnvironment(projectId, environmentId, options = {}) {
|
|
522
|
+
return this.get(`/projects/${encode(projectId)}/environments/${encode(environmentId)}`, options);
|
|
523
|
+
}
|
|
504
524
|
listPipelineJobs(projectId, pipelineId, options = {}) {
|
|
505
525
|
return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/jobs`, options);
|
|
506
526
|
}
|
|
@@ -513,6 +533,47 @@ export class GitLabClient {
|
|
|
513
533
|
getPipelineJobOutput(projectId, jobId, options = {}) {
|
|
514
534
|
return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/trace`, options);
|
|
515
535
|
}
|
|
536
|
+
listJobArtifacts(projectId, jobId, options = {}) {
|
|
537
|
+
return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts/tree`, options);
|
|
538
|
+
}
|
|
539
|
+
async downloadJobArtifacts(projectId, jobId, options = {}) {
|
|
540
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
541
|
+
const url = new URL(`projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts`, `${requestConfig.apiUrl}/`);
|
|
542
|
+
return this.downloadFile(url, {
|
|
543
|
+
headers: options.headers,
|
|
544
|
+
token: requestConfig.token,
|
|
545
|
+
authHeader: requestConfig.authHeader
|
|
546
|
+
}, "Job artifacts", `artifacts-job-${jobId}.zip`);
|
|
547
|
+
}
|
|
548
|
+
async saveJobArtifacts(projectId, jobId, localPath, options = {}) {
|
|
549
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
550
|
+
const url = new URL(`projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts`, `${requestConfig.apiUrl}/`);
|
|
551
|
+
return this.saveDownloadedFile(url, {
|
|
552
|
+
headers: options.headers,
|
|
553
|
+
token: requestConfig.token,
|
|
554
|
+
authHeader: requestConfig.authHeader
|
|
555
|
+
}, "Job artifacts", `artifacts-job-${jobId}.zip`, localPath);
|
|
556
|
+
}
|
|
557
|
+
async getJobArtifactFile(projectId, jobId, artifactPath, options = {}) {
|
|
558
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
559
|
+
const encodedArtifactPath = encodeSlashPath(artifactPath);
|
|
560
|
+
const url = new URL(`projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts/${encodedArtifactPath}`, `${requestConfig.apiUrl}/`);
|
|
561
|
+
return this.downloadFileContent(url, {
|
|
562
|
+
headers: options.headers,
|
|
563
|
+
token: requestConfig.token,
|
|
564
|
+
authHeader: requestConfig.authHeader
|
|
565
|
+
}, "Job artifact file", path.basename(artifactPath) || `artifact-${jobId}`);
|
|
566
|
+
}
|
|
567
|
+
async saveJobArtifactFile(projectId, jobId, artifactPath, localPath, options = {}) {
|
|
568
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
569
|
+
const encodedArtifactPath = encodeSlashPath(artifactPath);
|
|
570
|
+
const url = new URL(`projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts/${encodedArtifactPath}`, `${requestConfig.apiUrl}/`);
|
|
571
|
+
return this.saveDownloadedFile(url, {
|
|
572
|
+
headers: options.headers,
|
|
573
|
+
token: requestConfig.token,
|
|
574
|
+
authHeader: requestConfig.authHeader
|
|
575
|
+
}, "Job artifact file", path.basename(artifactPath) || `artifact-${jobId}`, localPath);
|
|
576
|
+
}
|
|
516
577
|
createPipeline(projectId, payload, options = {}) {
|
|
517
578
|
return this.post(`/projects/${encode(projectId)}/pipeline`, {
|
|
518
579
|
...options,
|
|
@@ -705,21 +766,126 @@ export class GitLabClient {
|
|
|
705
766
|
async downloadAttachment(urlOrPath, options = {}) {
|
|
706
767
|
const requestConfig = this.resolveRequestConfig(options);
|
|
707
768
|
const url = this.resolveAttachmentUrl(urlOrPath, requestConfig.apiUrl);
|
|
769
|
+
return this.downloadFile(url, {
|
|
770
|
+
headers: options.headers,
|
|
771
|
+
token: requestConfig.token,
|
|
772
|
+
authHeader: requestConfig.authHeader
|
|
773
|
+
}, "Attachment", `attachment-${Date.now()}`);
|
|
774
|
+
}
|
|
775
|
+
// graphql
|
|
776
|
+
executeGraphql(query, variables, options = {}) {
|
|
777
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
778
|
+
const endpoint = buildGraphqlEndpoint(requestConfig.apiUrl);
|
|
779
|
+
return this.rawRequest(endpoint, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
headers: {
|
|
782
|
+
"Content-Type": "application/json",
|
|
783
|
+
...(options.headers ?? {})
|
|
784
|
+
},
|
|
785
|
+
body: JSON.stringify({ query, variables }),
|
|
786
|
+
token: requestConfig.token,
|
|
787
|
+
authHeader: requestConfig.authHeader
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
async downloadFile(url, options, label, fallbackFileName) {
|
|
791
|
+
const response = await this.fetchRawResponse(url, {
|
|
792
|
+
method: "GET",
|
|
793
|
+
headers: options.headers,
|
|
794
|
+
token: options.token,
|
|
795
|
+
authHeader: options.authHeader
|
|
796
|
+
});
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
throw await this.toDownloadError(response, label);
|
|
799
|
+
}
|
|
800
|
+
assertContentLengthWithinLimit(response, this.maxAttachmentBytes, label);
|
|
801
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
802
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
803
|
+
const fileName = extractFileName(disposition) ?? fallbackFileName;
|
|
804
|
+
const bytes = await readResponseBytesWithLimit(response, this.maxAttachmentBytes, label);
|
|
805
|
+
return {
|
|
806
|
+
fileName,
|
|
807
|
+
contentType,
|
|
808
|
+
base64: bytes.toString("base64")
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
async saveDownloadedFile(url, options, label, fallbackFileName, localPath) {
|
|
812
|
+
const response = await this.fetchRawResponse(url, {
|
|
813
|
+
method: "GET",
|
|
814
|
+
headers: options.headers,
|
|
815
|
+
token: options.token,
|
|
816
|
+
authHeader: options.authHeader
|
|
817
|
+
});
|
|
818
|
+
if (!response.ok) {
|
|
819
|
+
throw await this.toDownloadError(response, label);
|
|
820
|
+
}
|
|
821
|
+
assertContentLengthWithinLimit(response, this.maxLocalFileBytes, label);
|
|
822
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
823
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
824
|
+
const resolvedFileName = resolveDownloadedFileName(disposition, fallbackFileName);
|
|
825
|
+
const baseDirectory = localPath ? path.resolve(localPath) : process.cwd();
|
|
826
|
+
const tempFilePath = buildTemporaryDownloadPath(baseDirectory, resolvedFileName);
|
|
827
|
+
await fs.mkdir(baseDirectory, { recursive: true });
|
|
828
|
+
const size = await writeResponseToFileWithLimit(response, tempFilePath, this.maxLocalFileBytes, label);
|
|
829
|
+
const filePath = await commitDownloadedFile(tempFilePath, baseDirectory, resolvedFileName);
|
|
830
|
+
const fileName = path.basename(filePath);
|
|
831
|
+
return {
|
|
832
|
+
filePath,
|
|
833
|
+
fileName,
|
|
834
|
+
contentType,
|
|
835
|
+
size
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
async downloadFileContent(url, options, label, fallbackFileName) {
|
|
839
|
+
const response = await this.fetchRawResponse(url, {
|
|
840
|
+
method: "GET",
|
|
841
|
+
headers: options.headers,
|
|
842
|
+
token: options.token,
|
|
843
|
+
authHeader: options.authHeader
|
|
844
|
+
});
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
throw await this.toDownloadError(response, label);
|
|
847
|
+
}
|
|
848
|
+
assertContentLengthWithinLimit(response, this.maxAttachmentBytes, label);
|
|
849
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
850
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
851
|
+
const fileName = extractFileName(disposition) ?? fallbackFileName;
|
|
852
|
+
const bytes = await readResponseBytesWithLimit(response, this.maxAttachmentBytes, label);
|
|
853
|
+
if (isTextLikeContent(contentType, fileName)) {
|
|
854
|
+
return {
|
|
855
|
+
fileName,
|
|
856
|
+
contentType,
|
|
857
|
+
encoding: "utf8",
|
|
858
|
+
content: bytes.toString("utf8")
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
fileName,
|
|
863
|
+
contentType,
|
|
864
|
+
encoding: "base64",
|
|
865
|
+
content: bytes.toString("base64")
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async fetchRawResponse(url, options) {
|
|
708
869
|
let headers = new Headers(options.headers);
|
|
709
|
-
let
|
|
710
|
-
let
|
|
870
|
+
let requestBody = options.body;
|
|
871
|
+
let token = options.token;
|
|
872
|
+
let authHeader = options.authHeader;
|
|
711
873
|
let fetchImpl = fetch;
|
|
712
874
|
if (this.beforeRequest) {
|
|
713
875
|
const override = await this.beforeRequest({
|
|
714
876
|
url,
|
|
715
|
-
method:
|
|
877
|
+
method: options.method,
|
|
716
878
|
headers,
|
|
879
|
+
body: requestBody,
|
|
717
880
|
token,
|
|
718
|
-
authHeader:
|
|
881
|
+
authHeader: options.authHeader
|
|
719
882
|
});
|
|
720
883
|
if (override?.headers) {
|
|
721
884
|
headers = override.headers;
|
|
722
885
|
}
|
|
886
|
+
if (override?.body !== undefined) {
|
|
887
|
+
requestBody = override.body;
|
|
888
|
+
}
|
|
723
889
|
if (override?.token !== undefined) {
|
|
724
890
|
token = override.token;
|
|
725
891
|
}
|
|
@@ -731,48 +897,24 @@ export class GitLabClient {
|
|
|
731
897
|
}
|
|
732
898
|
}
|
|
733
899
|
this.attachAuth(headers, token, authHeader);
|
|
734
|
-
|
|
735
|
-
method:
|
|
900
|
+
return fetchImpl(url, {
|
|
901
|
+
method: options.method,
|
|
902
|
+
body: requestBody,
|
|
736
903
|
headers,
|
|
737
904
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
738
905
|
});
|
|
739
|
-
if (!response.ok) {
|
|
740
|
-
let details;
|
|
741
|
-
try {
|
|
742
|
-
details = await this.parseResponseBody(response);
|
|
743
|
-
}
|
|
744
|
-
catch (error) {
|
|
745
|
-
details = {
|
|
746
|
-
message: error instanceof Error ? error.message : "Failed to read GitLab error response"
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
throw new GitLabApiError(`GitLab attachment download failed: ${response.status} ${response.statusText}`, response.status, details);
|
|
750
|
-
}
|
|
751
|
-
assertContentLengthWithinLimit(response, this.maxAttachmentBytes, "Attachment");
|
|
752
|
-
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
753
|
-
const disposition = response.headers.get("content-disposition") ?? "";
|
|
754
|
-
const fileName = extractFileName(disposition) ?? `attachment-${Date.now()}`;
|
|
755
|
-
const bytes = await readResponseBytesWithLimit(response, this.maxAttachmentBytes, "Attachment");
|
|
756
|
-
return {
|
|
757
|
-
fileName,
|
|
758
|
-
contentType,
|
|
759
|
-
base64: bytes.toString("base64")
|
|
760
|
-
};
|
|
761
906
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
token: requestConfig.token,
|
|
774
|
-
authHeader: requestConfig.authHeader
|
|
775
|
-
});
|
|
907
|
+
async toDownloadError(response, label) {
|
|
908
|
+
let details;
|
|
909
|
+
try {
|
|
910
|
+
details = await this.parseResponseBody(response);
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
details = {
|
|
914
|
+
message: error instanceof Error ? error.message : "Failed to read GitLab error response"
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return new GitLabApiError(`GitLab ${label.toLowerCase()} download failed: ${response.status} ${response.statusText}`, response.status, details);
|
|
776
918
|
}
|
|
777
919
|
// generic methods
|
|
778
920
|
get(path, options = {}) {
|
|
@@ -920,6 +1062,10 @@ export class GitLabClient {
|
|
|
920
1062
|
headers.set("Authorization", `Bearer ${token}`);
|
|
921
1063
|
return;
|
|
922
1064
|
}
|
|
1065
|
+
if (authHeader === "job-token") {
|
|
1066
|
+
headers.set("JOB-TOKEN", token);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
923
1069
|
headers.set("PRIVATE-TOKEN", token);
|
|
924
1070
|
}
|
|
925
1071
|
}
|
|
@@ -969,6 +1115,149 @@ function extractFileName(contentDisposition) {
|
|
|
969
1115
|
}
|
|
970
1116
|
return decodeURIComponent(quoted[1] ?? "");
|
|
971
1117
|
}
|
|
1118
|
+
function resolveDownloadedFileName(contentDisposition, fallbackFileName) {
|
|
1119
|
+
const extracted = extractFileName(contentDisposition);
|
|
1120
|
+
const sanitized = sanitizeDownloadedFileName(extracted);
|
|
1121
|
+
return sanitized ?? sanitizeDownloadedFileName(fallbackFileName) ?? "downloaded-file";
|
|
1122
|
+
}
|
|
1123
|
+
function sanitizeDownloadedFileName(fileName) {
|
|
1124
|
+
if (!fileName) {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
const normalized = fileName.replace(/\\/g, "/");
|
|
1128
|
+
const basename = path.posix.basename(normalized).trim();
|
|
1129
|
+
if (!basename || basename === "." || basename === "..") {
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
return basename;
|
|
1133
|
+
}
|
|
1134
|
+
async function commitDownloadedFile(tempFilePath, baseDirectory, fileName) {
|
|
1135
|
+
const parsed = path.parse(fileName);
|
|
1136
|
+
try {
|
|
1137
|
+
for (let suffix = 0; suffix < 10_000; suffix += 1) {
|
|
1138
|
+
const candidateName = suffix === 0 ? fileName : `${parsed.name || "downloaded-file"}-${suffix}${parsed.ext}`;
|
|
1139
|
+
const candidatePath = path.join(baseDirectory, candidateName);
|
|
1140
|
+
try {
|
|
1141
|
+
await fs.link(tempFilePath, candidatePath);
|
|
1142
|
+
return candidatePath;
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
if (isFileExistsError(error)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (!supportsAtomicLinkFallback(error)) {
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
await fs.copyFile(tempFilePath, candidatePath, fsConstants.COPYFILE_EXCL);
|
|
1153
|
+
return candidatePath;
|
|
1154
|
+
}
|
|
1155
|
+
catch (copyError) {
|
|
1156
|
+
if (isFileExistsError(copyError)) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
throw copyError;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
throw new Error(`Unable to find an available local file name for '${fileName}'`);
|
|
1164
|
+
}
|
|
1165
|
+
finally {
|
|
1166
|
+
await fs.rm(tempFilePath, { force: true });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function buildTemporaryDownloadPath(baseDirectory, fileName) {
|
|
1170
|
+
return path.join(baseDirectory, `.${fileName}.${randomUUID()}.tmp`);
|
|
1171
|
+
}
|
|
1172
|
+
function isFileExistsError(error) {
|
|
1173
|
+
return (typeof error === "object" &&
|
|
1174
|
+
error !== null &&
|
|
1175
|
+
"code" in error &&
|
|
1176
|
+
error.code === "EEXIST");
|
|
1177
|
+
}
|
|
1178
|
+
function supportsAtomicLinkFallback(error) {
|
|
1179
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
return ["EMLINK", "ENOTSUP", "EPERM"].includes(String(error.code));
|
|
1183
|
+
}
|
|
1184
|
+
const TEXT_CONTENT_TYPE_HINTS = [
|
|
1185
|
+
"application/json",
|
|
1186
|
+
"application/ld+json",
|
|
1187
|
+
"application/problem+json",
|
|
1188
|
+
"application/graphql",
|
|
1189
|
+
"application/javascript",
|
|
1190
|
+
"application/typescript",
|
|
1191
|
+
"application/xml",
|
|
1192
|
+
"application/xhtml+xml",
|
|
1193
|
+
"application/yaml",
|
|
1194
|
+
"application/x-yaml",
|
|
1195
|
+
"application/toml",
|
|
1196
|
+
"application/csv",
|
|
1197
|
+
"application/sql"
|
|
1198
|
+
];
|
|
1199
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
1200
|
+
".c",
|
|
1201
|
+
".cc",
|
|
1202
|
+
".cfg",
|
|
1203
|
+
".conf",
|
|
1204
|
+
".cpp",
|
|
1205
|
+
".cs",
|
|
1206
|
+
".css",
|
|
1207
|
+
".csv",
|
|
1208
|
+
".dockerfile",
|
|
1209
|
+
".env",
|
|
1210
|
+
".go",
|
|
1211
|
+
".graphql",
|
|
1212
|
+
".h",
|
|
1213
|
+
".hpp",
|
|
1214
|
+
".html",
|
|
1215
|
+
".ini",
|
|
1216
|
+
".java",
|
|
1217
|
+
".js",
|
|
1218
|
+
".json",
|
|
1219
|
+
".jsx",
|
|
1220
|
+
".kt",
|
|
1221
|
+
".kts",
|
|
1222
|
+
".log",
|
|
1223
|
+
".md",
|
|
1224
|
+
".mjs",
|
|
1225
|
+
".php",
|
|
1226
|
+
".properties",
|
|
1227
|
+
".py",
|
|
1228
|
+
".rb",
|
|
1229
|
+
".rs",
|
|
1230
|
+
".scss",
|
|
1231
|
+
".sh",
|
|
1232
|
+
".sql",
|
|
1233
|
+
".svg",
|
|
1234
|
+
".swift",
|
|
1235
|
+
".toml",
|
|
1236
|
+
".ts",
|
|
1237
|
+
".tsx",
|
|
1238
|
+
".txt",
|
|
1239
|
+
".xml",
|
|
1240
|
+
".yaml",
|
|
1241
|
+
".yml"
|
|
1242
|
+
]);
|
|
1243
|
+
function isTextLikeContent(contentType, fileName) {
|
|
1244
|
+
const normalizedContentType = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1245
|
+
if (normalizedContentType.startsWith("text/")) {
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
if (TEXT_CONTENT_TYPE_HINTS.includes(normalizedContentType)) {
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
return isTextLikeFileName(fileName);
|
|
1252
|
+
}
|
|
1253
|
+
function isTextLikeFileName(fileName) {
|
|
1254
|
+
const normalizedFileName = fileName.trim().toLowerCase();
|
|
1255
|
+
if (normalizedFileName === "dockerfile" || normalizedFileName.endsWith(".gitignore")) {
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
const extension = path.extname(normalizedFileName);
|
|
1259
|
+
return extension.length > 0 && TEXT_FILE_EXTENSIONS.has(extension);
|
|
1260
|
+
}
|
|
972
1261
|
function parseContentLength(value) {
|
|
973
1262
|
if (!value) {
|
|
974
1263
|
return undefined;
|
|
@@ -1022,4 +1311,53 @@ async function readResponseBytesWithLimit(response, maxBytes, label) {
|
|
|
1022
1311
|
}
|
|
1023
1312
|
return Buffer.concat(chunks, total);
|
|
1024
1313
|
}
|
|
1314
|
+
async function writeResponseToFileWithLimit(response, filePath, maxBytes, label) {
|
|
1315
|
+
if (!response.body) {
|
|
1316
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1317
|
+
if (bytes.length > maxBytes) {
|
|
1318
|
+
throw new Error(`${label} size ${bytes.length} bytes exceeds limit ${maxBytes} bytes`);
|
|
1319
|
+
}
|
|
1320
|
+
await fs.writeFile(filePath, bytes);
|
|
1321
|
+
return bytes.length;
|
|
1322
|
+
}
|
|
1323
|
+
const fileHandle = await fs.open(filePath, "w");
|
|
1324
|
+
const reader = response.body.getReader();
|
|
1325
|
+
let total = 0;
|
|
1326
|
+
let success = false;
|
|
1327
|
+
try {
|
|
1328
|
+
while (true) {
|
|
1329
|
+
const { done, value } = await reader.read();
|
|
1330
|
+
if (done) {
|
|
1331
|
+
success = true;
|
|
1332
|
+
return total;
|
|
1333
|
+
}
|
|
1334
|
+
if (!value) {
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
total += value.byteLength;
|
|
1338
|
+
if (total > maxBytes) {
|
|
1339
|
+
await reader.cancel();
|
|
1340
|
+
throw new Error(`${label} size ${total} bytes exceeds limit ${maxBytes} bytes`);
|
|
1341
|
+
}
|
|
1342
|
+
await writeBufferToFile(fileHandle, Buffer.from(value), total - value.byteLength);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
finally {
|
|
1346
|
+
reader.releaseLock();
|
|
1347
|
+
await fileHandle.close();
|
|
1348
|
+
if (!success) {
|
|
1349
|
+
await fs.rm(filePath, { force: true });
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async function writeBufferToFile(fileHandle, buffer, startPosition) {
|
|
1354
|
+
let offset = 0;
|
|
1355
|
+
while (offset < buffer.length) {
|
|
1356
|
+
const { bytesWritten } = await fileHandle.write(buffer, offset, buffer.length - offset, startPosition + offset);
|
|
1357
|
+
if (bytesWritten <= 0) {
|
|
1358
|
+
throw new Error("Failed to write download chunk");
|
|
1359
|
+
}
|
|
1360
|
+
offset += bytesWritten;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1025
1363
|
//# sourceMappingURL=gitlab-client.js.map
|