@flakiness/sdk 0.129.4 → 0.131.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/lib/cli/cli.js CHANGED
@@ -737,8 +737,50 @@ var Ranges;
737
737
  import { TypedHTTP as TypedHTTP4 } from "@flakiness/shared/common/typedHttp.js";
738
738
  import assert4 from "assert";
739
739
  import { Command, Option } from "commander";
740
- import fs12 from "fs";
741
- import path10 from "path";
740
+ import path11 from "path";
741
+
742
+ // ../package.json
743
+ var package_default = {
744
+ name: "flakiness",
745
+ version: "0.131.0",
746
+ private: true,
747
+ scripts: {
748
+ minor: "./version.mjs minor",
749
+ patch: "./version.mjs patch",
750
+ dev: "npx kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./stripe.mts",
751
+ "dev+billing": "npx kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe.mts",
752
+ prod: "npx kubik --env-file=.env.prodlocal -w ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts",
753
+ build: "npx kubik $(find . -name build.mts)",
754
+ perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
755
+ },
756
+ engines: {
757
+ node: ">=24"
758
+ },
759
+ author: "Degu Labs, Inc",
760
+ license: "Fair Source 100",
761
+ workspaces: [
762
+ "./report",
763
+ "./sdk",
764
+ "./docs",
765
+ "./landing",
766
+ "./devenv",
767
+ "./database",
768
+ "./server",
769
+ "./shared",
770
+ "./experimental",
771
+ "./e2e",
772
+ "./web"
773
+ ],
774
+ devDependencies: {
775
+ "@playwright/test": "^1.54.0",
776
+ "@types/node": "^22.10.2",
777
+ esbuild: "^0.27.0",
778
+ glob: "^10.3.10",
779
+ kubik: "^0.24.0",
780
+ tsx: "^4.19.2",
781
+ typescript: "^5.6.2"
782
+ }
783
+ };
742
784
 
743
785
  // src/flakinessConfig.ts
744
786
  import fs2 from "fs";
@@ -869,6 +911,29 @@ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:
869
911
  function stripAnsi(str) {
870
912
  return str.replace(ansiRegex, "");
871
913
  }
914
+ async function saveReportAndAttachments(report, attachments, outputFolder) {
915
+ const reportPath = path.join(outputFolder, "report.json");
916
+ const attachmentsFolder = path.join(outputFolder, "attachments");
917
+ await fs.promises.rm(outputFolder, { recursive: true, force: true });
918
+ await fs.promises.mkdir(outputFolder, { recursive: true });
919
+ await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
920
+ if (attachments.length)
921
+ await fs.promises.mkdir(attachmentsFolder);
922
+ const movedAttachments = [];
923
+ for (const attachment of attachments) {
924
+ const attachmentPath = path.join(attachmentsFolder, attachment.id);
925
+ if (attachment.path)
926
+ await fs.promises.cp(attachment.path, attachmentPath);
927
+ else if (attachment.body)
928
+ await fs.promises.writeFile(attachmentPath, attachment.body);
929
+ movedAttachments.push({
930
+ contentType: attachment.contentType,
931
+ id: attachment.id,
932
+ path: attachmentPath
933
+ });
934
+ }
935
+ return movedAttachments;
936
+ }
872
937
  function shell(command, args, options) {
873
938
  try {
874
939
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
@@ -1158,7 +1223,7 @@ import fs5 from "fs/promises";
1158
1223
  import path5 from "path";
1159
1224
 
1160
1225
  // src/junit.ts
1161
- import { ReportUtils as ReportUtils2, FlakinessReport as FK } from "@flakiness/report";
1226
+ import { ReportUtils as ReportUtils2 } from "@flakiness/report";
1162
1227
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
1163
1228
  import assert2 from "assert";
1164
1229
  import fs4 from "fs";
@@ -1236,7 +1301,7 @@ async function traverseJUnitReport(context, node) {
1236
1301
  file,
1237
1302
  line,
1238
1303
  column: 1
1239
- } : FK.NO_LOCATION,
1304
+ } : void 0,
1240
1305
  type: name ? "suite" : file ? "file" : "anonymous suite",
1241
1306
  suites: [],
1242
1307
  tests: []
@@ -1298,7 +1363,7 @@ async function traverseJUnitReport(context, node) {
1298
1363
  file,
1299
1364
  line,
1300
1365
  column: 1
1301
- } : FK.NO_LOCATION,
1366
+ } : void 0,
1302
1367
  attempts: [{
1303
1368
  environmentIdx: currentEnvIndex,
1304
1369
  expectedStatus,
@@ -1394,21 +1459,8 @@ async function cmdConvert(junitPath, options) {
1394
1459
  runStartTimestamp: Date.now(),
1395
1460
  runDuration: 0
1396
1461
  });
1397
- await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
1398
- console.log("\u2713 Saved report to fkreport.json");
1399
- if (attachments.length > 0) {
1400
- await fs5.mkdir("fkattachments", { recursive: true });
1401
- for (const attachment of attachments) {
1402
- if (attachment.path) {
1403
- const destPath = path5.join("fkattachments", attachment.id);
1404
- await fs5.copyFile(attachment.path, destPath);
1405
- } else if (attachment.body) {
1406
- const destPath = path5.join("fkattachments", attachment.id);
1407
- await fs5.writeFile(destPath, attachment.body);
1408
- }
1409
- }
1410
- console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
1411
- }
1462
+ await saveReportAndAttachments(report, attachments, options.outputDir);
1463
+ console.log(`\u2713 Saved to ${options.outputDir}`);
1412
1464
  }
1413
1465
  async function findXmlFiles(dir, result = []) {
1414
1466
  const entries = await fs5.readdir(dir, { withFileTypes: true });
@@ -1463,28 +1515,6 @@ async function cmdDownload(session2, project, runId) {
1463
1515
  console.log(`\u2714\uFE0F Saved as ${rootDir}`);
1464
1516
  }
1465
1517
 
1466
- // src/cli/cmd-link.ts
1467
- async function cmdLink(slug) {
1468
- const session2 = await FlakinessSession.load();
1469
- if (!session2) {
1470
- console.log(`Please login first`);
1471
- process.exit(1);
1472
- }
1473
- const [orgSlug, projectSlug] = slug.split("/");
1474
- const project = await session2.api.project.findProject.GET({
1475
- orgSlug,
1476
- projectSlug
1477
- });
1478
- if (!project) {
1479
- console.log(`Failed to find project ${slug}`);
1480
- process.exit(1);
1481
- }
1482
- const config = FlakinessConfig.createEmpty();
1483
- config.setProjectPublicId(project.projectPublicId);
1484
- await config.save();
1485
- console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
1486
- }
1487
-
1488
1518
  // ../server/lib/common/knownClientIds.js
1489
1519
  var KNOWN_CLIENT_IDS = {
1490
1520
  OFFICIAL_WEB: "flakiness-io-official-cli",
@@ -1507,7 +1537,8 @@ async function cmdLogout() {
1507
1537
  }
1508
1538
 
1509
1539
  // src/cli/cmd-login.ts
1510
- async function cmdLogin(endpoint) {
1540
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
1541
+ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
1511
1542
  await cmdLogout();
1512
1543
  const api = createServerAPI(endpoint);
1513
1544
  const data = await api.deviceauth.createRequest.POST({
@@ -1521,8 +1552,8 @@ async function cmdLogin(endpoint) {
1521
1552
  await new Promise((x) => setTimeout(x, 2e3));
1522
1553
  const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
1523
1554
  if (!result) {
1524
- console.log(`Authorization request was rejected.`);
1525
- return;
1555
+ console.error(`Authorization request was rejected.`);
1556
+ process.exit(1);
1526
1557
  }
1527
1558
  token = result.token;
1528
1559
  if (token)
@@ -1544,12 +1575,47 @@ async function cmdLogin(endpoint) {
1544
1575
  const message = e instanceof Error ? e.message : String(e);
1545
1576
  console.error(`x Failed to login:`, message);
1546
1577
  }
1578
+ return session2;
1579
+ }
1580
+
1581
+ // src/cli/cmd-link.ts
1582
+ async function cmdLink(slugOrUrl) {
1583
+ let slug = slugOrUrl;
1584
+ let endpoint = DEFAULT_FLAKINESS_ENDPOINT;
1585
+ if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
1586
+ const url = URL.parse(slugOrUrl);
1587
+ if (!url) {
1588
+ console.error(`Invalid URL: ${slugOrUrl}`);
1589
+ process.exit(1);
1590
+ }
1591
+ slug = url.pathname.substring(1);
1592
+ endpoint = url.origin;
1593
+ } else if (slugOrUrl.startsWith("flakiness.io/")) {
1594
+ endpoint = "https://flakiness.io";
1595
+ slug = slugOrUrl.substring("flakiness.io/".length);
1596
+ }
1597
+ let session2 = await FlakinessSession.load();
1598
+ if (!session2 || session2.endpoint() !== endpoint)
1599
+ session2 = await cmdLogin(endpoint);
1600
+ const [orgSlug, projectSlug] = slug.split("/");
1601
+ const project = await session2.api.project.findProject.GET({
1602
+ orgSlug,
1603
+ projectSlug
1604
+ });
1605
+ if (!project) {
1606
+ console.log(`Failed to find project ${slug}`);
1607
+ process.exit(1);
1608
+ }
1609
+ const config = FlakinessConfig.createEmpty();
1610
+ config.setProjectPublicId(project.projectPublicId);
1611
+ await config.save();
1612
+ console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
1547
1613
  }
1548
1614
 
1549
1615
  // src/cli/cmd-show-report.ts
1550
1616
  import chalk from "chalk";
1551
1617
  import open2 from "open";
1552
- import path7 from "path";
1618
+ import path8 from "path";
1553
1619
 
1554
1620
  // src/localReportServer.ts
1555
1621
  import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
@@ -1560,9 +1626,14 @@ import compression from "compression";
1560
1626
  import debug from "debug";
1561
1627
  import express from "express";
1562
1628
  import "express-async-errors";
1563
- import fs8 from "fs";
1564
1629
  import http2 from "http";
1565
1630
 
1631
+ // src/localReportApi.ts
1632
+ import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
1633
+ import fs7 from "fs";
1634
+ import path7 from "path";
1635
+ import { z } from "zod/v4";
1636
+
1566
1637
  // src/localGit.ts
1567
1638
  import { exec } from "child_process";
1568
1639
  import { promisify } from "util";
@@ -1604,9 +1675,37 @@ async function listLocalCommits(gitRoot, head, count) {
1604
1675
  }
1605
1676
 
1606
1677
  // src/localReportApi.ts
1607
- import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
1608
- import fs7 from "fs";
1609
- import { z } from "zod/v4";
1678
+ var ReportInfo = class {
1679
+ constructor(_options) {
1680
+ this._options = _options;
1681
+ }
1682
+ report;
1683
+ attachmentIdToPath = /* @__PURE__ */ new Map();
1684
+ commits = [];
1685
+ async refresh() {
1686
+ const report = await fs7.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
1687
+ if (!report) {
1688
+ this.report = void 0;
1689
+ this.commits = [];
1690
+ this.attachmentIdToPath = /* @__PURE__ */ new Map();
1691
+ return;
1692
+ }
1693
+ if (JSON.stringify(report) === JSON.stringify(this.report))
1694
+ return;
1695
+ this.report = report;
1696
+ this.commits = await listLocalCommits(path7.dirname(this._options.reportPath), report.commitId, 100);
1697
+ const attachmentsDir = this._options.attachmentsFolder;
1698
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1699
+ if (missingAttachments.length) {
1700
+ const first = missingAttachments.slice(0, 3);
1701
+ for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
1702
+ console.warn(`Missing attachment with id ${missingAttachments[i]}`);
1703
+ if (missingAttachments.length > 3)
1704
+ console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
1705
+ }
1706
+ this.attachmentIdToPath = attachmentIdToPath;
1707
+ }
1708
+ };
1610
1709
  var t = TypedHTTP2.Router.create();
1611
1710
  var localReportRouter = {
1612
1711
  ping: t.get({
@@ -1616,7 +1715,7 @@ var localReportRouter = {
1616
1715
  }),
1617
1716
  lastCommits: t.get({
1618
1717
  handler: async ({ ctx }) => {
1619
- return ctx.commits;
1718
+ return ctx.reportInfo.commits;
1620
1719
  }
1621
1720
  }),
1622
1721
  report: {
@@ -1625,7 +1724,7 @@ var localReportRouter = {
1625
1724
  attachmentId: z.string().min(1).max(100).transform((id) => id)
1626
1725
  }),
1627
1726
  handler: async ({ ctx, input }) => {
1628
- const idx = ctx.attachmentIdToPath.get(input.attachmentId);
1727
+ const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
1629
1728
  if (!idx)
1630
1729
  throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
1631
1730
  const buffer = await fs7.promises.readFile(idx.path);
@@ -1634,7 +1733,8 @@ var localReportRouter = {
1634
1733
  }),
1635
1734
  json: t.get({
1636
1735
  handler: async ({ ctx }) => {
1637
- return ctx.report;
1736
+ await ctx.reportInfo.refresh();
1737
+ return ctx.reportInfo.report;
1638
1738
  }
1639
1739
  })
1640
1740
  }
@@ -1649,17 +1749,6 @@ var LocalReportServer = class _LocalReportServer {
1649
1749
  this._authToken = _authToken;
1650
1750
  }
1651
1751
  static async create(options) {
1652
- const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
1653
- const attachmentsDir = options.attachmentsFolder;
1654
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1655
- if (missingAttachments.length) {
1656
- const first = missingAttachments.slice(0, 3);
1657
- for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
1658
- console.warn(`Missing attachment with id ${missingAttachments[i]}`);
1659
- if (missingAttachments.length > 3)
1660
- console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
1661
- }
1662
- const commits = await listLocalCommits(process.cwd(), report.commitId, 100);
1663
1752
  const app = express();
1664
1753
  app.set("etag", false);
1665
1754
  const authToken = randomUUIDBase62();
@@ -1682,9 +1771,10 @@ var LocalReportServer = class _LocalReportServer {
1682
1771
  });
1683
1772
  next();
1684
1773
  });
1774
+ const reportInfo = new ReportInfo(options);
1685
1775
  app.use("/" + authToken, createTypedHttpExpressMiddleware({
1686
1776
  router: localReportRouter,
1687
- createRootContext: async ({ req, res, input }) => ({ report, commits, attachmentIdToPath })
1777
+ createRootContext: async ({ req, res, input }) => ({ reportInfo })
1688
1778
  }));
1689
1779
  app.use((err2, req, res, next) => {
1690
1780
  if (err2 instanceof TypedHTTP3.HttpError)
@@ -1718,7 +1808,7 @@ var LocalReportServer = class _LocalReportServer {
1718
1808
 
1719
1809
  // src/cli/cmd-show-report.ts
1720
1810
  async function cmdShowReport(reportFolder) {
1721
- const reportPath = path7.join(reportFolder, "report.json");
1811
+ const reportPath = path8.join(reportFolder, "report.json");
1722
1812
  const session2 = await FlakinessSession.load();
1723
1813
  const config = await FlakinessConfig.load();
1724
1814
  const projectPublicId = config.projectPublicId();
@@ -1768,8 +1858,8 @@ async function cmdUnlink() {
1768
1858
  }
1769
1859
 
1770
1860
  // src/cli/cmd-upload-playwright-json.ts
1771
- import fs10 from "fs/promises";
1772
- import path8 from "path";
1861
+ import fs9 from "fs/promises";
1862
+ import path9 from "path";
1773
1863
 
1774
1864
  // src/playwrightJSONReport.ts
1775
1865
  import { FlakinessReport as FK2 } from "@flakiness/report";
@@ -1950,7 +2040,7 @@ function parseJSONError(context, error) {
1950
2040
  // src/reportUploader.ts
1951
2041
  import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
1952
2042
  import assert3 from "assert";
1953
- import fs9 from "fs";
2043
+ import fs8 from "fs";
1954
2044
  import { URL as URL2 } from "url";
1955
2045
  var ReportUploader = class _ReportUploader {
1956
2046
  static optionsFromEnv(overrides) {
@@ -2048,16 +2138,16 @@ var ReportUpload = class {
2048
2138
  url: uploadUrl,
2049
2139
  headers: {
2050
2140
  "Content-Type": attachment.contentType,
2051
- "Content-Length": (await fs9.promises.stat(attachmentPath)).size + ""
2141
+ "Content-Length": (await fs8.promises.stat(attachmentPath)).size + ""
2052
2142
  },
2053
2143
  method: "put"
2054
2144
  });
2055
- fs9.createReadStream(attachmentPath).pipe(request);
2145
+ fs8.createReadStream(attachmentPath).pipe(request);
2056
2146
  await responseDataPromise;
2057
2147
  }, HTTP_BACKOFF);
2058
2148
  return;
2059
2149
  }
2060
- let buffer = attachment.body ? attachment.body : attachment.path ? await fs9.promises.readFile(attachment.path) : void 0;
2150
+ let buffer = attachment.body ? attachment.body : attachment.path ? await fs8.promises.readFile(attachment.path) : void 0;
2061
2151
  assert3(buffer);
2062
2152
  const encoding = compressable ? "br" : void 0;
2063
2153
  if (compressable)
@@ -2082,12 +2172,12 @@ var ReportUpload = class {
2082
2172
 
2083
2173
  // src/cli/cmd-upload-playwright-json.ts
2084
2174
  async function cmdUploadPlaywrightJson(relativePath, options) {
2085
- const fullPath = path8.resolve(relativePath);
2086
- if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
2175
+ const fullPath = path9.resolve(relativePath);
2176
+ if (!await fs9.access(fullPath, fs9.constants.F_OK).then(() => true).catch(() => false)) {
2087
2177
  console.error(`Error: path ${fullPath} is not accessible`);
2088
2178
  process.exit(1);
2089
2179
  }
2090
- const text = await fs10.readFile(fullPath, "utf-8");
2180
+ const text = await fs9.readFile(fullPath, "utf-8");
2091
2181
  const playwrightJson = JSON.parse(text);
2092
2182
  const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
2093
2183
  extractAttachments: true
@@ -2109,20 +2199,20 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
2109
2199
 
2110
2200
  // src/cli/cmd-upload.ts
2111
2201
  import chalk2 from "chalk";
2112
- import fs11 from "fs/promises";
2113
- import path9 from "path";
2202
+ import fs10 from "fs/promises";
2203
+ import path10 from "path";
2114
2204
  var warn = (txt) => console.warn(chalk2.yellow(`[flakiness.io] WARN: ${txt}`));
2115
2205
  var err = (txt) => console.error(chalk2.red(`[flakiness.io] Error: ${txt}`));
2116
2206
  var log = (txt) => console.log(`[flakiness.io] ${txt}`);
2117
2207
  async function cmdUpload(relativePath, options) {
2118
- const fullPath = path9.resolve(relativePath);
2119
- if (!await fs11.access(fullPath, fs11.constants.F_OK).then(() => true).catch(() => false)) {
2208
+ const fullPath = path10.resolve(relativePath);
2209
+ if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
2120
2210
  err(`Path ${fullPath} is not accessible!`);
2121
2211
  process.exit(1);
2122
2212
  }
2123
- const text = await fs11.readFile(fullPath, "utf-8");
2213
+ const text = await fs10.readFile(fullPath, "utf-8");
2124
2214
  const report = JSON.parse(text);
2125
- const attachmentsDir = options.attachmentsDir ?? path9.dirname(fullPath);
2215
+ const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
2126
2216
  const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
2127
2217
  if (missingAttachments.length) {
2128
2218
  warn(`Missing ${missingAttachments.length} attachments`);
@@ -2155,7 +2245,7 @@ async function cmdWhoami() {
2155
2245
  // src/cli/cli.ts
2156
2246
  var session = await FlakinessSession.load();
2157
2247
  var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
2158
- var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
2248
+ var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
2159
2249
  var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
2160
2250
  async function runCommand(callback) {
2161
2251
  try {
@@ -2167,8 +2257,7 @@ async function runCommand(callback) {
2167
2257
  process.exit(1);
2168
2258
  }
2169
2259
  }
2170
- var PACKAGE_JSON = JSON.parse(fs12.readFileSync(path10.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
2171
- var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
2260
+ var program = new Command().name("flakiness").description("Flakiness CLI tool").version(package_default.version);
2172
2261
  async function ensureAccessToken(options) {
2173
2262
  let accessToken = options.accessToken;
2174
2263
  if (!accessToken) {
@@ -2195,7 +2284,7 @@ program.command("upload-playwright-json", { hidden: true }).description("Upload
2195
2284
  await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
2196
2285
  }));
2197
2286
  var optLink = new Option("--link <org/proj>", "A project to link to");
2198
- program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
2287
+ program.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
2199
2288
  await cmdLogin(options.endpoint);
2200
2289
  if (options.link)
2201
2290
  await cmdLink(options.link);
@@ -2206,11 +2295,11 @@ program.command("logout").description("Logout from current session").action(asyn
2206
2295
  program.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
2207
2296
  await cmdWhoami();
2208
2297
  }));
2209
- program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("org/project", 'An org and project slugs, e.g. "facebook/react"').action(async (slug, options) => runCommand(async () => {
2298
+ program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("flakiness.io/org/project", "A URL of the Flakiness.io project").action(async (slugOrUrl, options) => runCommand(async () => {
2210
2299
  const session2 = await FlakinessSession.load();
2211
2300
  if (!session2)
2212
2301
  await cmdLogin(options.endpoint);
2213
- await cmdLink(slug);
2302
+ await cmdLink(slugOrUrl);
2214
2303
  }));
2215
2304
  program.command("unlink").description("Unlink repository from the flakiness project").action(async () => runCommand(async () => {
2216
2305
  await cmdUnlink();
@@ -2268,11 +2357,11 @@ program.command("upload").description("Upload Flakiness report to the flakiness.
2268
2357
  await cmdUpload(relativePath, await ensureAccessToken(options));
2269
2358
  });
2270
2359
  });
2271
- program.command("show-report [report]").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`.").action(async (arg) => runCommand(async () => {
2272
- const dir = path10.join(process.cwd(), arg ?? "flakiness-report");
2360
+ 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 () => {
2361
+ const dir = path11.resolve(arg ?? "flakiness-report");
2273
2362
  await cmdShowReport(dir);
2274
2363
  }));
2275
- 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", "default").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").action(async (junitPath, options) => {
2364
+ 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) => {
2276
2365
  await runCommand(async () => {
2277
2366
  await cmdConvert(junitPath, options);
2278
2367
  });
@@ -2,14 +2,14 @@
2
2
 
3
3
  // src/cli/cmd-convert.ts
4
4
  import fs3 from "fs/promises";
5
- import path2 from "path";
5
+ import path3 from "path";
6
6
 
7
7
  // src/junit.ts
8
- import { ReportUtils as ReportUtils2, FlakinessReport as FK } from "@flakiness/report";
8
+ import { ReportUtils as ReportUtils2 } from "@flakiness/report";
9
9
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
10
10
  import assert2 from "assert";
11
11
  import fs2 from "fs";
12
- import path from "path";
12
+ import path2 from "path";
13
13
 
14
14
  // src/utils.ts
15
15
  import { ReportUtils } from "@flakiness/report";
@@ -19,6 +19,7 @@ import crypto from "crypto";
19
19
  import fs from "fs";
20
20
  import http from "http";
21
21
  import https from "https";
22
+ import path, { posix as posixPath, win32 as win32Path } from "path";
22
23
  function sha1File(filePath) {
23
24
  return new Promise((resolve, reject) => {
24
25
  const hash = crypto.createHash("sha1");
@@ -122,6 +123,29 @@ var httpUtils;
122
123
  httpUtils2.postJSON = postJSON;
123
124
  })(httpUtils || (httpUtils = {}));
124
125
  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");
126
+ async function saveReportAndAttachments(report, attachments, outputFolder) {
127
+ const reportPath = path.join(outputFolder, "report.json");
128
+ const attachmentsFolder = path.join(outputFolder, "attachments");
129
+ await fs.promises.rm(outputFolder, { recursive: true, force: true });
130
+ await fs.promises.mkdir(outputFolder, { recursive: true });
131
+ await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
132
+ if (attachments.length)
133
+ await fs.promises.mkdir(attachmentsFolder);
134
+ const movedAttachments = [];
135
+ for (const attachment of attachments) {
136
+ const attachmentPath = path.join(attachmentsFolder, attachment.id);
137
+ if (attachment.path)
138
+ await fs.promises.cp(attachment.path, attachmentPath);
139
+ else if (attachment.body)
140
+ await fs.promises.writeFile(attachmentPath, attachment.body);
141
+ movedAttachments.push({
142
+ contentType: attachment.contentType,
143
+ id: attachment.id,
144
+ path: attachmentPath
145
+ });
146
+ }
147
+ return movedAttachments;
148
+ }
125
149
  function shell(command, args, options) {
126
150
  try {
127
151
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
@@ -187,7 +211,7 @@ function extractStdout(testcase, stdio) {
187
211
  }));
188
212
  }
189
213
  async function parseAttachment(value) {
190
- let absolutePath = path.resolve(process.cwd(), value);
214
+ let absolutePath = path2.resolve(process.cwd(), value);
191
215
  if (fs2.existsSync(absolutePath)) {
192
216
  const id = await sha1File(absolutePath);
193
217
  return {
@@ -219,7 +243,7 @@ async function traverseJUnitReport(context, node) {
219
243
  file,
220
244
  line,
221
245
  column: 1
222
- } : FK.NO_LOCATION,
246
+ } : void 0,
223
247
  type: name ? "suite" : file ? "file" : "anonymous suite",
224
248
  suites: [],
225
249
  tests: []
@@ -260,7 +284,7 @@ async function traverseJUnitReport(context, node) {
260
284
  id: attachment.id,
261
285
  contentType: attachment.contentType,
262
286
  //TODO: better default names for attachments?
263
- name: attachment.path ? path.basename(attachment.path) : `attachment`
287
+ name: attachment.path ? path2.basename(attachment.path) : `attachment`
264
288
  });
265
289
  } else {
266
290
  annotations.push({
@@ -281,7 +305,7 @@ async function traverseJUnitReport(context, node) {
281
305
  file,
282
306
  line,
283
307
  column: 1
284
- } : FK.NO_LOCATION,
308
+ } : void 0,
285
309
  attempts: [{
286
310
  environmentIdx: currentEnvIndex,
287
311
  expectedStatus,
@@ -335,7 +359,7 @@ async function parseJUnit(xmls, options) {
335
359
 
336
360
  // src/cli/cmd-convert.ts
337
361
  async function cmdConvert(junitPath, options) {
338
- const fullPath = path2.resolve(junitPath);
362
+ const fullPath = path3.resolve(junitPath);
339
363
  if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
340
364
  console.error(`Error: path ${fullPath} is not accessible`);
341
365
  process.exit(1);
@@ -377,26 +401,13 @@ async function cmdConvert(junitPath, options) {
377
401
  runStartTimestamp: Date.now(),
378
402
  runDuration: 0
379
403
  });
380
- await fs3.writeFile("fkreport.json", JSON.stringify(report, null, 2));
381
- console.log("\u2713 Saved report to fkreport.json");
382
- if (attachments.length > 0) {
383
- await fs3.mkdir("fkattachments", { recursive: true });
384
- for (const attachment of attachments) {
385
- if (attachment.path) {
386
- const destPath = path2.join("fkattachments", attachment.id);
387
- await fs3.copyFile(attachment.path, destPath);
388
- } else if (attachment.body) {
389
- const destPath = path2.join("fkattachments", attachment.id);
390
- await fs3.writeFile(destPath, attachment.body);
391
- }
392
- }
393
- console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
394
- }
404
+ await saveReportAndAttachments(report, attachments, options.outputDir);
405
+ console.log(`\u2713 Saved to ${options.outputDir}`);
395
406
  }
396
407
  async function findXmlFiles(dir, result = []) {
397
408
  const entries = await fs3.readdir(dir, { withFileTypes: true });
398
409
  for (const entry of entries) {
399
- const fullPath = path2.join(dir, entry.name);
410
+ const fullPath = path3.join(dir, entry.name);
400
411
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
401
412
  result.push(fullPath);
402
413
  else if (entry.isDirectory())