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.
Files changed (40) hide show
  1. package/README.md +30 -27
  2. package/dist/config/dotenv.js +6 -2
  3. package/dist/config/dotenv.js.map +1 -1
  4. package/dist/config/env.d.ts +3 -1
  5. package/dist/config/env.js +15 -1
  6. package/dist/config/env.js.map +1 -1
  7. package/dist/http-app.js +10 -3
  8. package/dist/http-app.js.map +1 -1
  9. package/dist/http.js +5 -4
  10. package/dist/http.js.map +1 -1
  11. package/dist/index.js +5 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/auth-context.d.ts +2 -1
  14. package/dist/lib/auth-context.js.map +1 -1
  15. package/dist/lib/gitlab-client.d.ts +42 -8
  16. package/dist/lib/gitlab-client.js +380 -42
  17. package/dist/lib/gitlab-client.js.map +1 -1
  18. package/dist/lib/network.js +12 -6
  19. package/dist/lib/network.js.map +1 -1
  20. package/dist/lib/oauth-scopes.d.ts +2 -0
  21. package/dist/lib/oauth-scopes.js +16 -0
  22. package/dist/lib/oauth-scopes.js.map +1 -0
  23. package/dist/lib/regex.d.ts +5 -0
  24. package/dist/lib/regex.js +111 -0
  25. package/dist/lib/regex.js.map +1 -0
  26. package/dist/lib/request-runtime.js +24 -11
  27. package/dist/lib/request-runtime.js.map +1 -1
  28. package/dist/tools/gitlab.js +193 -3
  29. package/dist/tools/gitlab.js.map +1 -1
  30. package/dist/types/auth.d.ts +1 -0
  31. package/dist/types/auth.js +2 -0
  32. package/dist/types/auth.js.map +1 -0
  33. package/dist/types/context.d.ts +1 -0
  34. package/docs/architecture.md +1 -1
  35. package/docs/authentication.md +4 -1
  36. package/docs/configuration.md +23 -21
  37. package/docs/deployment.md +2 -0
  38. package/docs/mcp-integration-testing-best-practices.md +381 -730
  39. package/docs/tools.md +24 -14
  40. 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 token = requestConfig.token;
710
- let authHeader = requestConfig.authHeader;
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: "GET",
877
+ method: options.method,
716
878
  headers,
879
+ body: requestBody,
717
880
  token,
718
- authHeader: requestConfig.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
- const response = await fetchImpl(url, {
735
- method: "GET",
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
- // graphql
763
- executeGraphql(query, variables, options = {}) {
764
- const requestConfig = this.resolveRequestConfig(options);
765
- const endpoint = buildGraphqlEndpoint(requestConfig.apiUrl);
766
- return this.rawRequest(endpoint, {
767
- method: "POST",
768
- headers: {
769
- "Content-Type": "application/json",
770
- ...(options.headers ?? {})
771
- },
772
- body: JSON.stringify({ query, variables }),
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