flakiness 0.159.0 → 0.161.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/LICENSE CHANGED
@@ -1,45 +1,21 @@
1
- Fair Source License, version 0.9
2
-
3
- Copyright (C) 2023 Degu Labs, Inc.
4
-
5
- Licensor: Degu Labs, Inc
6
-
7
- Software: @flakiness/playwright-report
8
-
9
- Use Limitation: 100 users. Executing the software, modifying the code, or
10
- accessing a running copy of the software constitutes “use.”
11
-
12
- License Grant. Licensor hereby grants to each recipient of the
13
- Software ("you") a non-exclusive, non-transferable, royalty-free and
14
- fully-paid-up license, under all of the Licensor’s copyright and
15
- patent rights, to use, copy, distribute, prepare derivative works of,
16
- publicly perform and display the Software, subject to the Use
17
- Limitation and the conditions set forth below.
18
-
19
- Use Limitation. The license granted above allows use by up to the
20
- number of users per entity set forth above (the "Use Limitation"). For
21
- determining the number of users, "you" includes all affiliates,
22
- meaning legal entities controlling, controlled by, or under common
23
- control with you. If you exceed the Use Limitation, your use is
24
- subject to payment of Licensor’s then-current list price for licenses.
25
-
26
- Conditions. Redistribution in source code or other forms must include
27
- a copy of this license document to be provided in a reasonable
28
- manner. Any redistribution of the Software is only allowed subject to
29
- this license.
30
-
31
- Trademarks. This license does not grant you any right in the
32
- trademarks, service marks, brand names or logos of Licensor.
33
-
34
- DISCLAIMER. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OR
35
- CONDITION, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES
36
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
37
- NONINFRINGEMENT. LICENSORS HEREBY DISCLAIM ALL LIABILITY, WHETHER IN
38
- AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
39
- CONNECTION WITH THE SOFTWARE.
40
-
41
- Termination. If you violate the terms of this license, your rights
42
- will terminate automatically and will not be reinstated without the
43
- prior written consent of Licensor. Any such termination will not
44
- affect the right of others who may have received copies of the
45
- Software from you.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Degu Labs, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -65,7 +65,6 @@ flakiness upload ./report.json --endpoint https://custom.flakiness.io
65
65
  **Options:**
66
66
  - `-t, --access-token <token>` — Read-write access token (env: `FLAKINESS_ACCESS_TOKEN`)
67
67
  - `-e, --endpoint <url>` — Service endpoint (env: `FLAKINESS_ENDPOINT`)
68
- - `--attachments-dir <dir>` — Directory containing attachments (defaults to report directory)
69
68
 
70
69
  ### Download Reports
71
70
 
package/lib/cli/cli.js CHANGED
@@ -906,8 +906,8 @@ var TypedHTTP;
906
906
  }
907
907
  TypedHTTP2.Router = Router;
908
908
  function createClient(base, fetchCallback) {
909
- function buildUrl(method, path8, input, options) {
910
- const url = new URL(path8.join("/"), base);
909
+ function buildUrl(method, path7, input, options) {
910
+ const url = new URL(path7.join("/"), base);
911
911
  const signal = options?.signal;
912
912
  let body = void 0;
913
913
  if (method === "GET" && input)
@@ -922,8 +922,8 @@ var TypedHTTP;
922
922
  signal
923
923
  };
924
924
  }
925
- function fetcher(method, path8, input, methodOptions) {
926
- const options = buildUrl(method, path8, input, methodOptions);
925
+ function fetcher(method, path7, input, methodOptions) {
926
+ const options = buildUrl(method, path7, input, methodOptions);
927
927
  return fetchCallback(options.url, {
928
928
  method: options.method,
929
929
  body: options.body,
@@ -946,17 +946,17 @@ var TypedHTTP;
946
946
  }
947
947
  });
948
948
  }
949
- function createProxy(path8 = []) {
949
+ function createProxy(path7 = []) {
950
950
  return new Proxy({}, {
951
951
  get(target, prop) {
952
952
  if (typeof prop === "symbol")
953
953
  return void 0;
954
954
  if (allHttpMethods.includes(prop)) {
955
- const f = fetcher.bind(null, prop, path8);
956
- f.prepare = buildUrl.bind(null, prop, path8);
955
+ const f = fetcher.bind(null, prop, path7);
956
+ f.prepare = buildUrl.bind(null, prop, path7);
957
957
  return f;
958
958
  }
959
- const newPath = [...path8, prop];
959
+ const newPath = [...path7, prop];
960
960
  return createProxy(newPath);
961
961
  }
962
962
  });
@@ -969,14 +969,15 @@ var TypedHTTP;
969
969
  // src/cli/cli.ts
970
970
  import assert2 from "assert";
971
971
  import { Command, Option } from "commander";
972
- import fs7 from "fs";
972
+ import debug2 from "debug";
973
+ import fs6 from "fs";
973
974
  import ora from "ora";
974
- import path7 from "path";
975
+ import path6 from "path";
975
976
 
976
977
  // ../package.json
977
978
  var package_default = {
978
979
  name: "flakiness",
979
- version: "0.159.0",
980
+ version: "0.161.0",
980
981
  type: "module",
981
982
  private: true,
982
983
  scripts: {
@@ -1006,157 +1007,13 @@ var package_default = {
1006
1007
  };
1007
1008
 
1008
1009
  // src/flakinessSession.ts
1009
- import fs2 from "fs/promises";
1010
+ import fs from "fs/promises";
1010
1011
  import os from "os";
1011
- import path2 from "path";
1012
-
1013
- // src/utils.ts
1014
- import { ReportUtils } from "@flakiness/sdk";
1015
- import crypto from "crypto";
1016
- import fs from "fs";
1017
- import http from "http";
1018
- import https from "https";
1019
1012
  import path from "path";
1020
- function sha1File(filePath) {
1021
- return new Promise((resolve, reject) => {
1022
- const hash = crypto.createHash("sha1");
1023
- const stream = fs.createReadStream(filePath);
1024
- stream.on("data", (chunk) => {
1025
- hash.update(chunk);
1026
- });
1027
- stream.on("end", () => {
1028
- resolve(hash.digest("hex"));
1029
- });
1030
- stream.on("error", (err2) => {
1031
- reject(err2);
1032
- });
1033
- });
1034
- }
1035
- var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
1036
- function errorText(error) {
1037
- return FLAKINESS_DBG ? error.stack : error.message;
1038
- }
1039
- function sha1Buffer(data) {
1040
- const hash = crypto.createHash("sha1");
1041
- hash.update(data);
1042
- return hash.digest("hex");
1043
- }
1044
- async function retryWithBackoff(job, backoff = []) {
1045
- for (const timeout of backoff) {
1046
- try {
1047
- return await job();
1048
- } catch (e) {
1049
- if (e instanceof AggregateError)
1050
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
1051
- else if (e instanceof Error)
1052
- console.error(`[flakiness.io err]`, errorText(e));
1053
- else
1054
- console.error(`[flakiness.io err]`, e);
1055
- await new Promise((x) => setTimeout(x, timeout));
1056
- }
1057
- }
1058
- return await job();
1059
- }
1060
- var httpUtils;
1061
- ((httpUtils2) => {
1062
- function createRequest({ url, method = "get", headers = {} }) {
1063
- let resolve;
1064
- let reject;
1065
- const responseDataPromise = new Promise((a, b) => {
1066
- resolve = a;
1067
- reject = b;
1068
- });
1069
- const protocol = url.startsWith("https") ? https : http;
1070
- headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
1071
- const request = protocol.request(url, { method, headers }, (res) => {
1072
- const chunks = [];
1073
- res.on("data", (chunk) => chunks.push(chunk));
1074
- res.on("end", () => {
1075
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
1076
- resolve(Buffer.concat(chunks));
1077
- else
1078
- reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
1079
- });
1080
- res.on("error", (error) => reject(error));
1081
- });
1082
- request.on("error", reject);
1083
- return { request, responseDataPromise };
1084
- }
1085
- httpUtils2.createRequest = createRequest;
1086
- async function getBuffer(url, backoff) {
1087
- return await retryWithBackoff(async () => {
1088
- const { request, responseDataPromise } = createRequest({ url });
1089
- request.end();
1090
- return await responseDataPromise;
1091
- }, backoff);
1092
- }
1093
- httpUtils2.getBuffer = getBuffer;
1094
- async function getText(url, backoff) {
1095
- const buffer = await getBuffer(url, backoff);
1096
- return buffer.toString("utf-8");
1097
- }
1098
- httpUtils2.getText = getText;
1099
- async function getJSON(url) {
1100
- return JSON.parse(await getText(url));
1101
- }
1102
- httpUtils2.getJSON = getJSON;
1103
- async function postText(url, text, backoff) {
1104
- const headers = {
1105
- "Content-Type": "application/json",
1106
- "Content-Length": Buffer.byteLength(text) + ""
1107
- };
1108
- return await retryWithBackoff(async () => {
1109
- const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
1110
- request.write(text);
1111
- request.end();
1112
- return await responseDataPromise;
1113
- }, backoff);
1114
- }
1115
- httpUtils2.postText = postText;
1116
- async function postJSON(url, json, backoff) {
1117
- const buffer = await postText(url, JSON.stringify(json), backoff);
1118
- return JSON.parse(buffer.toString("utf-8"));
1119
- }
1120
- httpUtils2.postJSON = postJSON;
1121
- })(httpUtils || (httpUtils = {}));
1122
- var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
1123
- async function resolveAttachmentPaths(report, attachmentsDir) {
1124
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
1125
- const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
1126
- const attachmentIdToPath = /* @__PURE__ */ new Map();
1127
- const missingAttachments = /* @__PURE__ */ new Set();
1128
- ReportUtils.visitTests(report, (test) => {
1129
- for (const attempt of test.attempts) {
1130
- for (const attachment of attempt.attachments ?? []) {
1131
- const attachmentPath = filenameToPath.get(attachment.id);
1132
- if (!attachmentPath) {
1133
- missingAttachments.add(attachment.id);
1134
- } else {
1135
- attachmentIdToPath.set(attachment.id, {
1136
- contentType: attachment.contentType,
1137
- id: attachment.id,
1138
- path: attachmentPath,
1139
- type: "file"
1140
- });
1141
- }
1142
- }
1143
- }
1144
- });
1145
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
1146
- }
1147
- async function listFilesRecursively(dir, result = []) {
1148
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1149
- for (const entry of entries) {
1150
- const fullPath = path.join(dir, entry.name);
1151
- if (entry.isDirectory())
1152
- await listFilesRecursively(fullPath, result);
1153
- else
1154
- result.push(fullPath);
1155
- }
1156
- return result;
1157
- }
1158
1013
 
1159
1014
  // src/serverapi.ts
1015
+ import debug from "debug";
1016
+ var log = debug("fk:server_api");
1160
1017
  function createServerAPI(endpoint, options) {
1161
1018
  endpoint += "/api/";
1162
1019
  const fetcher = options?.auth ? (url, init) => fetch(url, {
@@ -1170,13 +1027,29 @@ function createServerAPI(endpoint, options) {
1170
1027
  return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
1171
1028
  return TypedHTTP.createClient(endpoint, fetcher);
1172
1029
  }
1030
+ async function retryWithBackoff(job, backoff = []) {
1031
+ for (const timeout of backoff) {
1032
+ try {
1033
+ return await job();
1034
+ } catch (e) {
1035
+ if (e instanceof AggregateError)
1036
+ console.error(`[flakiness.io err]`, log(e.errors[0]));
1037
+ else if (e instanceof Error)
1038
+ console.error(`[flakiness.io err]`, log(e));
1039
+ else
1040
+ console.error(`[flakiness.io err]`, e);
1041
+ await new Promise((x) => setTimeout(x, timeout));
1042
+ }
1043
+ }
1044
+ return await job();
1045
+ }
1173
1046
 
1174
1047
  // src/flakinessSession.ts
1175
1048
  var CONFIG_DIR = (() => {
1176
- const configDir = process.platform === "darwin" ? path2.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path2.join(os.homedir(), "AppData", "Roaming", "flakiness") : path2.join(os.homedir(), ".config", "flakiness");
1049
+ const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
1177
1050
  return configDir;
1178
1051
  })();
1179
- var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
1052
+ var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
1180
1053
  var FlakinessSession = class _FlakinessSession {
1181
1054
  constructor(_config) {
1182
1055
  this._config = _config;
@@ -1189,14 +1062,14 @@ var FlakinessSession = class _FlakinessSession {
1189
1062
  return session2;
1190
1063
  }
1191
1064
  static async load() {
1192
- const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
1065
+ const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
1193
1066
  if (!data)
1194
1067
  return void 0;
1195
1068
  const json = JSON.parse(data);
1196
1069
  return new _FlakinessSession(json);
1197
1070
  }
1198
1071
  static async remove() {
1199
- await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
1072
+ await fs.unlink(CONFIG_PATH).catch((e) => void 0);
1200
1073
  }
1201
1074
  api;
1202
1075
  endpoint() {
@@ -1209,22 +1082,23 @@ var FlakinessSession = class _FlakinessSession {
1209
1082
  return this._config.token;
1210
1083
  }
1211
1084
  async save() {
1212
- await fs2.mkdir(CONFIG_DIR, { recursive: true });
1213
- await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
1085
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
1086
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
1214
1087
  }
1215
1088
  };
1216
1089
 
1217
1090
  // src/cli/cmd-convert.ts
1218
1091
  import { GitWorktree, writeReport } from "@flakiness/sdk";
1219
- import fs4 from "fs/promises";
1220
- import path4 from "path";
1092
+ import fs3 from "fs/promises";
1093
+ import path3 from "path";
1221
1094
 
1222
1095
  // src/junit.ts
1223
- import { ReportUtils as ReportUtils2 } from "@flakiness/sdk";
1096
+ import { ReportUtils } from "@flakiness/sdk";
1224
1097
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
1225
1098
  import assert from "assert";
1226
- import fs3 from "fs";
1227
- import path3 from "path";
1099
+ import fs2 from "fs";
1100
+ import mime from "mime";
1101
+ import path2 from "path";
1228
1102
  function getProperties(element) {
1229
1103
  const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
1230
1104
  if (!propertiesNodes.length)
@@ -1266,22 +1140,10 @@ function extractStdout(testcase, stdio) {
1266
1140
  }));
1267
1141
  }
1268
1142
  async function parseAttachment(value) {
1269
- let absolutePath = path3.resolve(process.cwd(), value);
1270
- if (fs3.existsSync(absolutePath)) {
1271
- const id = await sha1File(absolutePath);
1272
- return {
1273
- contentType: "image/png",
1274
- path: absolutePath,
1275
- id,
1276
- type: "file"
1277
- };
1278
- }
1279
- return {
1280
- contentType: "text/plain",
1281
- id: sha1Buffer(value),
1282
- body: Buffer.from(value),
1283
- type: "buffer"
1284
- };
1143
+ let absolutePath = path2.resolve(process.cwd(), value);
1144
+ if (fs2.existsSync(absolutePath))
1145
+ return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
1146
+ return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
1285
1147
  }
1286
1148
  async function traverseJUnitReport(context, node) {
1287
1149
  const element = node;
@@ -1342,7 +1204,7 @@ async function traverseJUnitReport(context, node) {
1342
1204
  id: attachment.id,
1343
1205
  contentType: attachment.contentType,
1344
1206
  //TODO: better default names for attachments?
1345
- name: attachment.type === "file" ? path3.basename(attachment.path) : `attachment`
1207
+ name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
1346
1208
  });
1347
1209
  } else {
1348
1210
  annotations.push({
@@ -1410,22 +1272,22 @@ async function parseJUnit(xmls, options) {
1410
1272
  await traverseJUnitReport(context, element);
1411
1273
  }
1412
1274
  return {
1413
- report: ReportUtils2.normalizeReport(report),
1275
+ report: ReportUtils.normalizeReport(report),
1414
1276
  attachments: Array.from(context.attachments.values())
1415
1277
  };
1416
1278
  }
1417
1279
 
1418
1280
  // src/cli/cmd-convert.ts
1419
1281
  async function cmdConvert(junitPath, options) {
1420
- const fullPath = path4.resolve(junitPath);
1421
- if (!await fs4.access(fullPath, fs4.constants.F_OK).then(() => true).catch(() => false)) {
1282
+ const fullPath = path3.resolve(junitPath);
1283
+ if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
1422
1284
  console.error(`Error: path ${fullPath} is not accessible`);
1423
1285
  process.exit(1);
1424
1286
  }
1425
- const stat = await fs4.stat(fullPath);
1287
+ const stat = await fs3.stat(fullPath);
1426
1288
  let xmlContents = [];
1427
1289
  if (stat.isFile()) {
1428
- const xmlContent = await fs4.readFile(fullPath, "utf-8");
1290
+ const xmlContent = await fs3.readFile(fullPath, "utf-8");
1429
1291
  xmlContents.push(xmlContent);
1430
1292
  } else if (stat.isDirectory()) {
1431
1293
  const xmlFiles = await findXmlFiles(fullPath);
@@ -1435,7 +1297,7 @@ async function cmdConvert(junitPath, options) {
1435
1297
  }
1436
1298
  console.log(`Found ${xmlFiles.length} XML files`);
1437
1299
  for (const xmlFile of xmlFiles) {
1438
- const xmlContent = await fs4.readFile(xmlFile, "utf-8");
1300
+ const xmlContent = await fs3.readFile(xmlFile, "utf-8");
1439
1301
  xmlContents.push(xmlContent);
1440
1302
  }
1441
1303
  } else {
@@ -1464,9 +1326,9 @@ async function cmdConvert(junitPath, options) {
1464
1326
  console.log(`\u2713 Saved to ${options.outputDir}`);
1465
1327
  }
1466
1328
  async function findXmlFiles(dir, result = []) {
1467
- const entries = await fs4.readdir(dir, { withFileTypes: true });
1329
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
1468
1330
  for (const entry of entries) {
1469
- const fullPath = path4.join(dir, entry.name);
1331
+ const fullPath = path3.join(dir, entry.name);
1470
1332
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
1471
1333
  result.push(fullPath);
1472
1334
  else if (entry.isDirectory())
@@ -1476,10 +1338,10 @@ async function findXmlFiles(dir, result = []) {
1476
1338
  }
1477
1339
 
1478
1340
  // src/cli/cmd-download.ts
1479
- import fs5 from "fs";
1480
- import path5 from "path";
1341
+ import fs4 from "fs";
1342
+ import path4 from "path";
1481
1343
  async function cmdDownload(session2, project, runId, rootDir) {
1482
- if (fs5.existsSync(rootDir)) {
1344
+ if (fs4.existsSync(rootDir)) {
1483
1345
  console.log(`Directory ${rootDir} already exists!`);
1484
1346
  return;
1485
1347
  }
@@ -1488,15 +1350,15 @@ async function cmdDownload(session2, project, runId, rootDir) {
1488
1350
  projectSlug: project.projectSlug,
1489
1351
  runId
1490
1352
  });
1491
- const attachmentsDir = path5.join(rootDir, "attachments");
1492
- await fs5.promises.mkdir(rootDir, { recursive: true });
1353
+ const attachmentsDir = path4.join(rootDir, "attachments");
1354
+ await fs4.promises.mkdir(rootDir, { recursive: true });
1493
1355
  if (urls.attachmentURLs.length)
1494
- await fs5.promises.mkdir(attachmentsDir, { recursive: true });
1356
+ await fs4.promises.mkdir(attachmentsDir, { recursive: true });
1495
1357
  const response = await fetch(urls.reportURL);
1496
1358
  if (!response.ok)
1497
1359
  throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
1498
1360
  const reportContent = await response.text();
1499
- await fs5.promises.writeFile(path5.join(rootDir, "report.json"), reportContent);
1361
+ await fs4.promises.writeFile(path4.join(rootDir, "report.json"), reportContent);
1500
1362
  const attachmentDownloader = async () => {
1501
1363
  while (urls.attachmentURLs.length) {
1502
1364
  const url = urls.attachmentURLs.pop();
@@ -1504,8 +1366,8 @@ async function cmdDownload(session2, project, runId, rootDir) {
1504
1366
  if (!response2.ok)
1505
1367
  throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
1506
1368
  const fileBuffer = Buffer.from(await response2.arrayBuffer());
1507
- const filename = path5.basename(new URL(url).pathname);
1508
- await fs5.promises.writeFile(path5.join(attachmentsDir, filename), fileBuffer);
1369
+ const filename = path4.basename(new URL(url).pathname);
1370
+ await fs4.promises.writeFile(path4.join(attachmentsDir, filename), fileBuffer);
1509
1371
  }
1510
1372
  };
1511
1373
  const workerPromises = [];
@@ -1626,27 +1488,23 @@ async function cmdUnlink() {
1626
1488
  }
1627
1489
 
1628
1490
  // src/cli/cmd-upload.ts
1629
- import { uploadReport } from "@flakiness/sdk";
1491
+ import { readReport, uploadReport } from "@flakiness/sdk";
1630
1492
  import chalk from "chalk";
1631
- import fs6 from "fs/promises";
1632
- import path6 from "path";
1493
+ import fs5 from "fs/promises";
1494
+ import path5 from "path";
1633
1495
  var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
1634
1496
  var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
1635
1497
  async function cmdUpload(relativePaths, options) {
1636
1498
  for (const relativePath of relativePaths) {
1637
- const fullPath = path6.resolve(relativePath);
1638
- if (!await fs6.access(fullPath, fs6.constants.F_OK).then(() => true).catch(() => false)) {
1499
+ const fullPath = path5.resolve(relativePath);
1500
+ if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
1639
1501
  err(`Path ${fullPath} is not accessible!`);
1640
1502
  process.exit(1);
1641
1503
  }
1642
- const text = await fs6.readFile(fullPath, "utf-8");
1643
- const report = JSON.parse(text);
1644
- const attachmentsDir = options.attachmentsDir ?? path6.dirname(fullPath);
1645
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1646
- if (missingAttachments.length) {
1504
+ const { report, attachments, missingAttachments } = await readReport(path5.dirname(fullPath));
1505
+ if (missingAttachments.length)
1647
1506
  warn(`Missing ${missingAttachments.length} attachments`);
1648
- }
1649
- await uploadReport(report, Array.from(attachmentIdToPath.values()), {
1507
+ await uploadReport(report, attachments, {
1650
1508
  flakinessAccessToken: options.accessToken,
1651
1509
  flakinessEndpoint: options.endpoint
1652
1510
  });
@@ -1666,17 +1524,17 @@ async function cmdWhoami() {
1666
1524
  }
1667
1525
 
1668
1526
  // src/cli/cli.ts
1527
+ var log2 = debug2("fk:cli");
1669
1528
  var session = await FlakinessSession.load();
1670
1529
  var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
1671
1530
  var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
1672
- var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
1673
1531
  async function runCommand(callback) {
1674
1532
  try {
1675
1533
  await callback();
1676
1534
  } catch (e) {
1677
1535
  if (!(e instanceof Error))
1678
1536
  throw e;
1679
- console.error(errorText(e));
1537
+ log2(e);
1680
1538
  process.exit(1);
1681
1539
  }
1682
1540
  }
@@ -1780,7 +1638,7 @@ program.command("download").description("Download run").addOption(optSince).addO
1780
1638
  });
1781
1639
  console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
1782
1640
  }
1783
- const alreadyExisting = fs7.readdirSync(process.cwd());
1641
+ const alreadyExisting = fs6.readdirSync(process.cwd());
1784
1642
  const downloadedRuns = alreadyExisting.filter((entry) => entry.startsWith("fkrun-")).map((run) => parseInt(run.substring(`fkrun-`.length), 10)).filter((runId) => !isNaN(runId));
1785
1643
  console.log(`Found ${downloadedRuns.length} locally downloaded reports`);
1786
1644
  const toBeDownloaded = Ranges.subtract(runIds, Ranges.fromList(downloadedRuns));
@@ -1803,13 +1661,13 @@ program.command("download").description("Download run").addOption(optSince).addO
1803
1661
  await Promise.all(downloaders);
1804
1662
  spinner.stop();
1805
1663
  }));
1806
- program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-paths...>", "Paths to the Flakiness report files").addOption(optAccessToken).addOption(optEndpoint).addOption(optAttachmentsDir).action(async (relativePaths, options) => {
1664
+ program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-paths...>", "Paths to the Flakiness report files").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePaths, options) => {
1807
1665
  await runCommand(async () => {
1808
1666
  await cmdUpload(relativePaths, await ensureAccessToken(options));
1809
1667
  });
1810
1668
  });
1811
1669
  program.command("show").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`. (default: flakiness-report)").action(async (arg) => runCommand(async () => {
1812
- const dir = path7.resolve(arg ?? "flakiness-report");
1670
+ const dir = path6.resolve(arg ?? "flakiness-report");
1813
1671
  await showReport(dir);
1814
1672
  }));
1815
1673
  program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "junit").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").option("--output-dir <dir>", "Output directory for the report", "flakiness-report").action(async (junitPath, options) => {