@daghis/teamcity-mcp 1.4.0 → 1.5.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/CHANGELOG.md +7 -0
- package/dist/index.js +416 -11
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/Daghis/teamcity-mcp/compare/v1.4.0...v1.5.0) (2025-09-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **teamcity:** add streaming artifact downloads ([#161](https://github.com/Daghis/teamcity-mcp/issues/161)) ([b50b773](https://github.com/Daghis/teamcity-mcp/commit/b50b773ebee6f5cc85f4c53c98fef525e7098fe9)), closes [#151](https://github.com/Daghis/teamcity-mcp/issues/151)
|
|
9
|
+
|
|
3
10
|
## [1.4.0](https://github.com/Daghis/teamcity-mcp/compare/v1.3.5...v1.4.0) (2025-09-20)
|
|
4
11
|
|
|
5
12
|
|
package/dist/index.js
CHANGED
|
@@ -758,9 +758,9 @@ var TeamCityLogger = class _TeamCityLogger {
|
|
|
758
758
|
*/
|
|
759
759
|
ensureLogDirectory(directory) {
|
|
760
760
|
try {
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
763
|
-
|
|
761
|
+
const fs2 = require("fs");
|
|
762
|
+
if (fs2.existsSync(directory) === false) {
|
|
763
|
+
fs2.mkdirSync(directory, { recursive: true });
|
|
764
764
|
}
|
|
765
765
|
} catch (error2) {
|
|
766
766
|
this.winston?.warn?.("Failed to create log directory, using current directory", { error: error2 });
|
|
@@ -921,6 +921,11 @@ function debug(message, meta) {
|
|
|
921
921
|
}
|
|
922
922
|
|
|
923
923
|
// src/tools.ts
|
|
924
|
+
var import_node_crypto = require("node:crypto");
|
|
925
|
+
var import_node_fs = require("node:fs");
|
|
926
|
+
var import_node_os = require("node:os");
|
|
927
|
+
var import_node_path = require("node:path");
|
|
928
|
+
var import_promises = require("node:stream/promises");
|
|
924
929
|
var import_axios35 = require("axios");
|
|
925
930
|
var import_zod4 = require("zod");
|
|
926
931
|
|
|
@@ -931,6 +936,318 @@ var ResolutionTypeEnum = {
|
|
|
931
936
|
AtTime: "atTime"
|
|
932
937
|
};
|
|
933
938
|
|
|
939
|
+
// src/teamcity/utils/build-locator.ts
|
|
940
|
+
var toBuildLocator = (buildId) => buildId.includes(":") ? buildId : `id:${buildId}`;
|
|
941
|
+
|
|
942
|
+
// src/teamcity/artifact-manager.ts
|
|
943
|
+
var ArtifactManager = class _ArtifactManager {
|
|
944
|
+
client;
|
|
945
|
+
cache = /* @__PURE__ */ new Map();
|
|
946
|
+
static cacheTtlMs = 6e4;
|
|
947
|
+
// 1 minute
|
|
948
|
+
static defaultLimit = 100;
|
|
949
|
+
static maxLimit = 1e3;
|
|
950
|
+
constructor(client) {
|
|
951
|
+
this.client = client;
|
|
952
|
+
}
|
|
953
|
+
getBaseUrl() {
|
|
954
|
+
const baseUrl = this.client.getApiConfig().baseUrl;
|
|
955
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* List artifacts for a build
|
|
959
|
+
*/
|
|
960
|
+
async listArtifacts(buildId, options = {}) {
|
|
961
|
+
const cacheKey = this.getCacheKey(buildId, options);
|
|
962
|
+
if (!options.forceRefresh) {
|
|
963
|
+
const cached = this.getFromCache(cacheKey);
|
|
964
|
+
if (cached) {
|
|
965
|
+
return cached;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const buildLocator = toBuildLocator(buildId);
|
|
970
|
+
const response = await this.client.modules.builds.getFilesListOfBuild(
|
|
971
|
+
buildLocator,
|
|
972
|
+
void 0,
|
|
973
|
+
void 0,
|
|
974
|
+
"file(name,fullName,size,modificationTime,href,children(file(name,fullName,size,modificationTime,href)))"
|
|
975
|
+
);
|
|
976
|
+
const baseUrl = this.getBaseUrl();
|
|
977
|
+
let artifacts = this.parseArtifacts(
|
|
978
|
+
response.data ?? {},
|
|
979
|
+
buildId,
|
|
980
|
+
options.includeNested,
|
|
981
|
+
baseUrl
|
|
982
|
+
);
|
|
983
|
+
artifacts = this.applyFilters(artifacts, options);
|
|
984
|
+
if (options.limit ?? options.offset) {
|
|
985
|
+
artifacts = this.paginate(
|
|
986
|
+
artifacts,
|
|
987
|
+
options.offset ?? 0,
|
|
988
|
+
options.limit ?? _ArtifactManager.defaultLimit
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
this.cacheResult(cacheKey, artifacts);
|
|
992
|
+
return artifacts;
|
|
993
|
+
} catch (error2) {
|
|
994
|
+
const err = error2;
|
|
995
|
+
if (err.response?.status === 401) {
|
|
996
|
+
throw new Error("Authentication failed: Invalid TeamCity token");
|
|
997
|
+
}
|
|
998
|
+
if (err.response?.status === 404) {
|
|
999
|
+
throw new Error(`Build not found: ${buildId}`);
|
|
1000
|
+
}
|
|
1001
|
+
const errMsg = err.message ?? String(error2);
|
|
1002
|
+
throw new Error(`Failed to fetch artifacts: ${errMsg}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Download a specific artifact
|
|
1007
|
+
*/
|
|
1008
|
+
async downloadArtifact(buildId, artifactPath, options = {}) {
|
|
1009
|
+
const artifacts = await this.listArtifacts(buildId);
|
|
1010
|
+
const artifact = artifacts.find((a) => a.path === artifactPath || a.name === artifactPath);
|
|
1011
|
+
if (!artifact) {
|
|
1012
|
+
throw new Error(`Artifact not found: ${artifactPath}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (options.maxSize && artifact.size > options.maxSize) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Artifact size exceeds maximum allowed size: ${artifact.size} > ${options.maxSize}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const encoding = options.encoding ?? "buffer";
|
|
1021
|
+
const normalizedPath = artifact.path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1022
|
+
const buildLocator = toBuildLocator(buildId);
|
|
1023
|
+
const artifactRequestPath = `content/${normalizedPath}`;
|
|
1024
|
+
if (encoding === "text") {
|
|
1025
|
+
const response2 = await this.client.modules.builds.downloadFileOfBuild(
|
|
1026
|
+
artifactRequestPath,
|
|
1027
|
+
buildLocator,
|
|
1028
|
+
void 0,
|
|
1029
|
+
void 0,
|
|
1030
|
+
{ responseType: "text" }
|
|
1031
|
+
);
|
|
1032
|
+
const axiosResponse2 = response2;
|
|
1033
|
+
const { data, headers } = axiosResponse2;
|
|
1034
|
+
if (typeof data !== "string") {
|
|
1035
|
+
throw new Error("Artifact download returned a non-text payload when text was expected");
|
|
1036
|
+
}
|
|
1037
|
+
const mimeType = typeof headers?.["content-type"] === "string" ? headers["content-type"] : void 0;
|
|
1038
|
+
return {
|
|
1039
|
+
name: artifact.name,
|
|
1040
|
+
path: artifact.path,
|
|
1041
|
+
size: artifact.size,
|
|
1042
|
+
content: data,
|
|
1043
|
+
mimeType
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (encoding === "stream") {
|
|
1047
|
+
const response2 = await this.client.modules.builds.downloadFileOfBuild(
|
|
1048
|
+
artifactRequestPath,
|
|
1049
|
+
buildLocator,
|
|
1050
|
+
void 0,
|
|
1051
|
+
void 0,
|
|
1052
|
+
{ responseType: "stream" }
|
|
1053
|
+
);
|
|
1054
|
+
const axiosResponse2 = response2;
|
|
1055
|
+
const stream = axiosResponse2.data;
|
|
1056
|
+
if (!this.isReadableStream(stream)) {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
"Artifact download returned a non-stream payload when stream was requested"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const mimeType = typeof axiosResponse2.headers?.["content-type"] === "string" ? axiosResponse2.headers["content-type"] : void 0;
|
|
1062
|
+
return {
|
|
1063
|
+
name: artifact.name,
|
|
1064
|
+
path: artifact.path,
|
|
1065
|
+
size: artifact.size,
|
|
1066
|
+
content: stream,
|
|
1067
|
+
mimeType
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const response = await this.client.modules.builds.downloadFileOfBuild(
|
|
1071
|
+
artifactRequestPath,
|
|
1072
|
+
buildLocator,
|
|
1073
|
+
void 0,
|
|
1074
|
+
void 0,
|
|
1075
|
+
{ responseType: "arraybuffer" }
|
|
1076
|
+
);
|
|
1077
|
+
const axiosResponse = response;
|
|
1078
|
+
const buffer = this.ensureBinaryBuffer(axiosResponse.data);
|
|
1079
|
+
let content;
|
|
1080
|
+
if (encoding === "base64") {
|
|
1081
|
+
content = buffer.toString("base64");
|
|
1082
|
+
} else {
|
|
1083
|
+
content = buffer;
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
name: artifact.name,
|
|
1087
|
+
path: artifact.path,
|
|
1088
|
+
size: artifact.size,
|
|
1089
|
+
content,
|
|
1090
|
+
mimeType: typeof axiosResponse.headers?.["content-type"] === "string" ? axiosResponse.headers["content-type"] : void 0
|
|
1091
|
+
};
|
|
1092
|
+
} catch (error2) {
|
|
1093
|
+
const errMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1094
|
+
throw new Error(`Failed to download artifact: ${errMsg}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Download multiple artifacts
|
|
1099
|
+
*/
|
|
1100
|
+
async downloadMultipleArtifacts(buildId, artifactPaths, options = {}) {
|
|
1101
|
+
if (options.encoding === "stream") {
|
|
1102
|
+
throw new Error("Streaming downloads are only supported when requesting a single artifact");
|
|
1103
|
+
}
|
|
1104
|
+
const downloadOptions = { encoding: "base64", ...options };
|
|
1105
|
+
const results = await Promise.allSettled(
|
|
1106
|
+
artifactPaths.map((path) => this.downloadArtifact(buildId, path, downloadOptions))
|
|
1107
|
+
);
|
|
1108
|
+
return results.map((result, index) => {
|
|
1109
|
+
if (result.status === "fulfilled") {
|
|
1110
|
+
return result.value;
|
|
1111
|
+
} else {
|
|
1112
|
+
const fallbackName = artifactPaths[index] ?? "unknown";
|
|
1113
|
+
return {
|
|
1114
|
+
name: fallbackName,
|
|
1115
|
+
path: fallbackName,
|
|
1116
|
+
size: 0,
|
|
1117
|
+
error: result.reason.message
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Parse artifacts from API response
|
|
1124
|
+
*/
|
|
1125
|
+
parseArtifacts(data, buildId, includeNested, baseUrl) {
|
|
1126
|
+
const artifacts = [];
|
|
1127
|
+
const files = data.file ?? [];
|
|
1128
|
+
for (const file of files) {
|
|
1129
|
+
if (file.children && includeNested) {
|
|
1130
|
+
const nested = this.parseArtifacts(file.children, buildId, includeNested, baseUrl);
|
|
1131
|
+
artifacts.push(...nested);
|
|
1132
|
+
} else if (!file.children) {
|
|
1133
|
+
artifacts.push({
|
|
1134
|
+
name: file.name ?? "",
|
|
1135
|
+
path: file.fullName ?? file.name ?? "",
|
|
1136
|
+
size: file.size ?? 0,
|
|
1137
|
+
modificationTime: file.modificationTime ?? "",
|
|
1138
|
+
downloadUrl: `${baseUrl}/app/rest/builds/id:${buildId}/artifacts/content/${file.fullName ?? file.name ?? ""}`,
|
|
1139
|
+
isDirectory: false
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return artifacts;
|
|
1144
|
+
}
|
|
1145
|
+
ensureBinaryBuffer(payload) {
|
|
1146
|
+
if (Buffer.isBuffer(payload)) {
|
|
1147
|
+
return payload;
|
|
1148
|
+
}
|
|
1149
|
+
if (payload instanceof ArrayBuffer) {
|
|
1150
|
+
return Buffer.from(payload);
|
|
1151
|
+
}
|
|
1152
|
+
throw new Error("Artifact download returned unexpected binary payload type");
|
|
1153
|
+
}
|
|
1154
|
+
isReadableStream(value) {
|
|
1155
|
+
if (value == null || typeof value !== "object") {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const candidate = value;
|
|
1159
|
+
return typeof candidate.pipe === "function";
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Apply filters to artifacts
|
|
1163
|
+
*/
|
|
1164
|
+
applyFilters(artifacts, options) {
|
|
1165
|
+
let filtered = artifacts;
|
|
1166
|
+
if (options.nameFilter) {
|
|
1167
|
+
const regex = this.globToRegex(options.nameFilter);
|
|
1168
|
+
filtered = filtered.filter((a) => regex.test(a.name));
|
|
1169
|
+
}
|
|
1170
|
+
if (options.pathFilter) {
|
|
1171
|
+
const regex = this.globToRegex(options.pathFilter);
|
|
1172
|
+
filtered = filtered.filter((a) => regex.test(a.path));
|
|
1173
|
+
}
|
|
1174
|
+
if (options.extension) {
|
|
1175
|
+
const ext = options.extension.startsWith(".") ? options.extension : `.${options.extension}`;
|
|
1176
|
+
filtered = filtered.filter((a) => a.name.endsWith(ext));
|
|
1177
|
+
}
|
|
1178
|
+
if (options.minSize !== void 0) {
|
|
1179
|
+
const minSize = options.minSize;
|
|
1180
|
+
filtered = filtered.filter((a) => a.size >= minSize);
|
|
1181
|
+
}
|
|
1182
|
+
if (options.maxSize !== void 0) {
|
|
1183
|
+
const maxSize = options.maxSize;
|
|
1184
|
+
filtered = filtered.filter((a) => a.size <= maxSize);
|
|
1185
|
+
}
|
|
1186
|
+
return filtered;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Convert glob pattern to regex
|
|
1190
|
+
*/
|
|
1191
|
+
globToRegex(pattern) {
|
|
1192
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1193
|
+
return new RegExp(`^${escaped}$`);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Paginate results
|
|
1197
|
+
*/
|
|
1198
|
+
paginate(artifacts, offset, limit) {
|
|
1199
|
+
const effectiveLimit = Math.min(limit, _ArtifactManager.maxLimit);
|
|
1200
|
+
return artifacts.slice(offset, offset + effectiveLimit);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Generate cache key
|
|
1204
|
+
*/
|
|
1205
|
+
getCacheKey(buildId, options) {
|
|
1206
|
+
const { forceRefresh: _forceRefresh, ...cacheOptions } = options;
|
|
1207
|
+
return `${buildId}:${JSON.stringify(cacheOptions)}`;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get from cache if valid
|
|
1211
|
+
*/
|
|
1212
|
+
getFromCache(key) {
|
|
1213
|
+
const entry = this.cache.get(key);
|
|
1214
|
+
if (!entry) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
const age = Date.now() - entry.timestamp;
|
|
1218
|
+
if (age > _ArtifactManager.cacheTtlMs) {
|
|
1219
|
+
this.cache.delete(key);
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
return entry.artifacts;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Cache artifacts
|
|
1226
|
+
*/
|
|
1227
|
+
cacheResult(key, artifacts) {
|
|
1228
|
+
this.cache.set(key, {
|
|
1229
|
+
artifacts,
|
|
1230
|
+
timestamp: Date.now()
|
|
1231
|
+
});
|
|
1232
|
+
this.cleanCache();
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Remove expired cache entries
|
|
1236
|
+
*/
|
|
1237
|
+
cleanCache() {
|
|
1238
|
+
const now = Date.now();
|
|
1239
|
+
const expired = [];
|
|
1240
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
1241
|
+
if (now - entry.timestamp > _ArtifactManager.cacheTtlMs) {
|
|
1242
|
+
expired.push(key);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
for (const key of expired) {
|
|
1246
|
+
this.cache.delete(key);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
|
|
934
1251
|
// src/teamcity/build-configuration-update-manager.ts
|
|
935
1252
|
var BuildConfigurationUpdateManager = class {
|
|
936
1253
|
client;
|
|
@@ -1317,9 +1634,6 @@ var BuildConfigurationUpdateManager = class {
|
|
|
1317
1634
|
}
|
|
1318
1635
|
};
|
|
1319
1636
|
|
|
1320
|
-
// src/teamcity/utils/build-locator.ts
|
|
1321
|
-
var toBuildLocator = (buildId) => buildId.includes(":") ? buildId : `id:${buildId}`;
|
|
1322
|
-
|
|
1323
1637
|
// src/teamcity/build-results-manager.ts
|
|
1324
1638
|
var BuildResultsManager = class _BuildResultsManager {
|
|
1325
1639
|
client;
|
|
@@ -1802,7 +2116,7 @@ function createAdapterFromTeamCityAPI(api, options = {}) {
|
|
|
1802
2116
|
listTestFailures: (buildId) => api.listTestFailures(buildId),
|
|
1803
2117
|
builds: buildApi,
|
|
1804
2118
|
listBuildArtifacts: (buildId, options2) => api.listBuildArtifacts(buildId, options2),
|
|
1805
|
-
downloadArtifactContent: (buildId, artifactPath) => api.downloadBuildArtifact(buildId, artifactPath),
|
|
2119
|
+
downloadArtifactContent: (buildId, artifactPath, requestOptions) => api.downloadBuildArtifact(buildId, artifactPath, requestOptions),
|
|
1806
2120
|
getBuildStatistics: (buildId, fields) => api.getBuildStatistics(buildId, fields),
|
|
1807
2121
|
listChangesForBuild: (buildId, fields) => api.listChangesForBuild(buildId, fields),
|
|
1808
2122
|
listSnapshotDependencies: (buildId) => api.listSnapshotDependencies(buildId),
|
|
@@ -36371,13 +36685,15 @@ var TeamCityAPI = class _TeamCityAPI {
|
|
|
36371
36685
|
options?.logBuildUsage
|
|
36372
36686
|
);
|
|
36373
36687
|
}
|
|
36374
|
-
async downloadBuildArtifact(buildId, artifactPath) {
|
|
36688
|
+
async downloadBuildArtifact(buildId, artifactPath, options) {
|
|
36375
36689
|
const normalizedPath = artifactPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
36690
|
+
const requestOptions = {
|
|
36691
|
+
...options ?? {},
|
|
36692
|
+
responseType: options?.responseType ?? "arraybuffer"
|
|
36693
|
+
};
|
|
36376
36694
|
return this.axiosInstance.get(
|
|
36377
36695
|
`/app/rest/builds/id:${buildId}/artifacts/content/${normalizedPath}`,
|
|
36378
|
-
|
|
36379
|
-
responseType: "arraybuffer"
|
|
36380
|
-
}
|
|
36696
|
+
requestOptions
|
|
36381
36697
|
);
|
|
36382
36698
|
}
|
|
36383
36699
|
async getBuildStatistics(buildId, fields) {
|
|
@@ -37972,6 +38288,95 @@ var DEV_TOOLS = [
|
|
|
37972
38288
|
);
|
|
37973
38289
|
}
|
|
37974
38290
|
},
|
|
38291
|
+
{
|
|
38292
|
+
name: "download_build_artifact",
|
|
38293
|
+
description: "Download a single artifact with optional streaming output",
|
|
38294
|
+
inputSchema: {
|
|
38295
|
+
type: "object",
|
|
38296
|
+
properties: {
|
|
38297
|
+
buildId: { type: "string", description: "Build ID" },
|
|
38298
|
+
artifactPath: { type: "string", description: "Artifact path or name" },
|
|
38299
|
+
encoding: {
|
|
38300
|
+
type: "string",
|
|
38301
|
+
description: "Response encoding: 'base64' (default), 'text', or 'stream'",
|
|
38302
|
+
enum: ["base64", "text", "stream"],
|
|
38303
|
+
default: "base64"
|
|
38304
|
+
},
|
|
38305
|
+
maxSize: {
|
|
38306
|
+
type: "number",
|
|
38307
|
+
description: "Maximum artifact size (bytes) allowed before aborting"
|
|
38308
|
+
},
|
|
38309
|
+
outputPath: {
|
|
38310
|
+
type: "string",
|
|
38311
|
+
description: "Optional absolute path to write streamed content; defaults to a temp file when streaming"
|
|
38312
|
+
}
|
|
38313
|
+
},
|
|
38314
|
+
required: ["buildId", "artifactPath"]
|
|
38315
|
+
},
|
|
38316
|
+
handler: async (args) => {
|
|
38317
|
+
const schema = import_zod4.z.object({
|
|
38318
|
+
buildId: import_zod4.z.string().min(1),
|
|
38319
|
+
artifactPath: import_zod4.z.string().min(1),
|
|
38320
|
+
encoding: import_zod4.z.enum(["base64", "text", "stream"]).default("base64"),
|
|
38321
|
+
maxSize: import_zod4.z.number().int().positive().optional(),
|
|
38322
|
+
outputPath: import_zod4.z.string().min(1).optional()
|
|
38323
|
+
});
|
|
38324
|
+
const isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
|
|
38325
|
+
const toTempFilePath = (artifactName) => {
|
|
38326
|
+
const base = (0, import_node_path.basename)(artifactName || "artifact");
|
|
38327
|
+
const safeStem = base.replace(/[^a-zA-Z0-9._-]/g, "_") || "artifact";
|
|
38328
|
+
const ext = (0, import_node_path.extname)(safeStem);
|
|
38329
|
+
const stemWithoutExt = ext ? safeStem.slice(0, -ext.length) : safeStem;
|
|
38330
|
+
const finalStem = stemWithoutExt || "artifact";
|
|
38331
|
+
const fileName = `${finalStem}-${(0, import_node_crypto.randomUUID)()}${ext}`;
|
|
38332
|
+
return (0, import_node_path.join)((0, import_node_os.tmpdir)(), fileName);
|
|
38333
|
+
};
|
|
38334
|
+
return runTool(
|
|
38335
|
+
"download_build_artifact",
|
|
38336
|
+
schema,
|
|
38337
|
+
async (typed) => {
|
|
38338
|
+
const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
|
|
38339
|
+
const manager = new ArtifactManager(adapter);
|
|
38340
|
+
const artifact = await manager.downloadArtifact(typed.buildId, typed.artifactPath, {
|
|
38341
|
+
encoding: typed.encoding,
|
|
38342
|
+
maxSize: typed.maxSize
|
|
38343
|
+
});
|
|
38344
|
+
if (typed.encoding === "stream") {
|
|
38345
|
+
const stream = artifact.content;
|
|
38346
|
+
if (!isReadableStream(stream)) {
|
|
38347
|
+
throw new Error("Streaming download did not return a readable stream");
|
|
38348
|
+
}
|
|
38349
|
+
const targetPath = typed.outputPath ?? toTempFilePath(artifact.name);
|
|
38350
|
+
await import_node_fs.promises.mkdir((0, import_node_path.dirname)(targetPath), { recursive: true });
|
|
38351
|
+
await (0, import_promises.pipeline)(stream, (0, import_node_fs.createWriteStream)(targetPath));
|
|
38352
|
+
const stats = await import_node_fs.promises.stat(targetPath);
|
|
38353
|
+
return json({
|
|
38354
|
+
name: artifact.name,
|
|
38355
|
+
path: artifact.path,
|
|
38356
|
+
size: artifact.size,
|
|
38357
|
+
mimeType: artifact.mimeType,
|
|
38358
|
+
encoding: "stream",
|
|
38359
|
+
outputPath: targetPath,
|
|
38360
|
+
bytesWritten: stats.size
|
|
38361
|
+
});
|
|
38362
|
+
}
|
|
38363
|
+
const payloadContent = artifact.content;
|
|
38364
|
+
if (typeof payloadContent !== "string") {
|
|
38365
|
+
throw new Error(`Expected ${typed.encoding} artifact content as string`);
|
|
38366
|
+
}
|
|
38367
|
+
return json({
|
|
38368
|
+
name: artifact.name,
|
|
38369
|
+
path: artifact.path,
|
|
38370
|
+
size: artifact.size,
|
|
38371
|
+
mimeType: artifact.mimeType,
|
|
38372
|
+
encoding: typed.encoding,
|
|
38373
|
+
content: payloadContent
|
|
38374
|
+
});
|
|
38375
|
+
},
|
|
38376
|
+
args
|
|
38377
|
+
);
|
|
38378
|
+
}
|
|
38379
|
+
},
|
|
37975
38380
|
{
|
|
37976
38381
|
name: "get_test_details",
|
|
37977
38382
|
description: "Get detailed information about test failures",
|