@flakiness/sdk 0.130.0 → 0.132.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
@@ -742,7 +742,7 @@ import path11 from "path";
742
742
  // ../package.json
743
743
  var package_default = {
744
744
  name: "flakiness",
745
- version: "0.130.0",
745
+ version: "0.132.0",
746
746
  private: true,
747
747
  scripts: {
748
748
  minor: "./version.mjs minor",
@@ -911,6 +911,29 @@ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:
911
911
  function stripAnsi(str) {
912
912
  return str.replace(ansiRegex, "");
913
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
+ }
914
937
  function shell(command, args, options) {
915
938
  try {
916
939
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
@@ -1436,21 +1459,8 @@ async function cmdConvert(junitPath, options) {
1436
1459
  runStartTimestamp: Date.now(),
1437
1460
  runDuration: 0
1438
1461
  });
1439
- await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
1440
- console.log("\u2713 Saved report to fkreport.json");
1441
- if (attachments.length > 0) {
1442
- await fs5.mkdir("fkattachments", { recursive: true });
1443
- for (const attachment of attachments) {
1444
- if (attachment.path) {
1445
- const destPath = path5.join("fkattachments", attachment.id);
1446
- await fs5.copyFile(attachment.path, destPath);
1447
- } else if (attachment.body) {
1448
- const destPath = path5.join("fkattachments", attachment.id);
1449
- await fs5.writeFile(destPath, attachment.body);
1450
- }
1451
- }
1452
- console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
1453
- }
1462
+ await saveReportAndAttachments(report, attachments, options.outputDir);
1463
+ console.log(`\u2713 Saved to ${options.outputDir}`);
1454
1464
  }
1455
1465
  async function findXmlFiles(dir, result = []) {
1456
1466
  const entries = await fs5.readdir(dir, { withFileTypes: true });
@@ -1506,12 +1516,7 @@ async function cmdDownload(session2, project, runId) {
1506
1516
  }
1507
1517
 
1508
1518
  // src/cli/cmd-link.ts
1509
- async function cmdLink(slug) {
1510
- const session2 = await FlakinessSession.load();
1511
- if (!session2) {
1512
- console.log(`Please login first`);
1513
- process.exit(1);
1514
- }
1519
+ async function cmdLink(session2, slug) {
1515
1520
  const [orgSlug, projectSlug] = slug.split("/");
1516
1521
  const project = await session2.api.project.findProject.GET({
1517
1522
  orgSlug,
@@ -1544,12 +1549,13 @@ async function cmdLogout() {
1544
1549
  return;
1545
1550
  const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
1546
1551
  if (currentSession)
1547
- await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
1552
+ await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
1548
1553
  await FlakinessSession.remove();
1549
1554
  }
1550
1555
 
1551
1556
  // src/cli/cmd-login.ts
1552
- async function cmdLogin(endpoint) {
1557
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
1558
+ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
1553
1559
  await cmdLogout();
1554
1560
  const api = createServerAPI(endpoint);
1555
1561
  const data = await api.deviceauth.createRequest.POST({
@@ -1563,8 +1569,8 @@ async function cmdLogin(endpoint) {
1563
1569
  await new Promise((x) => setTimeout(x, 2e3));
1564
1570
  const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
1565
1571
  if (!result) {
1566
- console.log(`Authorization request was rejected.`);
1567
- return;
1572
+ console.error(`Authorization request was rejected.`);
1573
+ process.exit(1);
1568
1574
  }
1569
1575
  token = result.token;
1570
1576
  if (token)
@@ -1586,6 +1592,7 @@ async function cmdLogin(endpoint) {
1586
1592
  const message = e instanceof Error ? e.message : String(e);
1587
1593
  console.error(`x Failed to login:`, message);
1588
1594
  }
1595
+ return session2;
1589
1596
  }
1590
1597
 
1591
1598
  // src/cli/cmd-show-report.ts
@@ -1624,8 +1631,10 @@ async function listLocalCommits(gitRoot, head, count) {
1624
1631
  // %at: Author date as a Unix timestamp (seconds since epoch)
1625
1632
  "%an",
1626
1633
  // %an: Author name
1627
- "%s"
1634
+ "%s",
1628
1635
  // %s: Subject (the first line of the commit message)
1636
+ "%P"
1637
+ // %P: Parent hashes (space-separated)
1629
1638
  ].join(FIELD_SEPARATOR);
1630
1639
  const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
1631
1640
  try {
@@ -1634,13 +1643,14 @@ async function listLocalCommits(gitRoot, head, count) {
1634
1643
  return [];
1635
1644
  }
1636
1645
  return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
1637
- const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
1646
+ const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
1647
+ const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
1638
1648
  return {
1639
1649
  commitId,
1640
1650
  timestamp: parseInt(timestampStr, 10) * 1e3,
1641
- // Convert timestamp from seconds to milliseconds
1642
1651
  author,
1643
1652
  message,
1653
+ parents,
1644
1654
  walkIndex: 0
1645
1655
  };
1646
1656
  });
@@ -2180,29 +2190,31 @@ import path10 from "path";
2180
2190
  var warn = (txt) => console.warn(chalk2.yellow(`[flakiness.io] WARN: ${txt}`));
2181
2191
  var err = (txt) => console.error(chalk2.red(`[flakiness.io] Error: ${txt}`));
2182
2192
  var log = (txt) => console.log(`[flakiness.io] ${txt}`);
2183
- async function cmdUpload(relativePath, options) {
2184
- const fullPath = path10.resolve(relativePath);
2185
- if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
2186
- err(`Path ${fullPath} is not accessible!`);
2187
- process.exit(1);
2188
- }
2189
- const text = await fs10.readFile(fullPath, "utf-8");
2190
- const report = JSON.parse(text);
2191
- const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
2192
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
2193
- if (missingAttachments.length) {
2194
- warn(`Missing ${missingAttachments.length} attachments`);
2195
- }
2193
+ async function cmdUpload(relativePaths, options) {
2196
2194
  const uploader = new ReportUploader({
2197
2195
  flakinessAccessToken: options.accessToken,
2198
2196
  flakinessEndpoint: options.endpoint
2199
2197
  });
2200
- const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
2201
- const uploadResult = await upload.upload();
2202
- if (!uploadResult.success) {
2203
- err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
2204
- } else {
2205
- log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
2198
+ for (const relativePath of relativePaths) {
2199
+ const fullPath = path10.resolve(relativePath);
2200
+ if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
2201
+ err(`Path ${fullPath} is not accessible!`);
2202
+ process.exit(1);
2203
+ }
2204
+ const text = await fs10.readFile(fullPath, "utf-8");
2205
+ const report = JSON.parse(text);
2206
+ const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
2207
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
2208
+ if (missingAttachments.length) {
2209
+ warn(`Missing ${missingAttachments.length} attachments`);
2210
+ }
2211
+ const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
2212
+ const uploadResult = await upload.upload();
2213
+ if (!uploadResult.success) {
2214
+ err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
2215
+ } else {
2216
+ log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
2217
+ }
2206
2218
  }
2207
2219
  }
2208
2220
 
@@ -2221,7 +2233,7 @@ async function cmdWhoami() {
2221
2233
  // src/cli/cli.ts
2222
2234
  var session = await FlakinessSession.load();
2223
2235
  var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
2224
- var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
2236
+ var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
2225
2237
  var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
2226
2238
  async function runCommand(callback) {
2227
2239
  try {
@@ -2259,11 +2271,8 @@ async function ensureAccessToken(options) {
2259
2271
  program.command("upload-playwright-json", { hidden: true }).description("Upload Playwright Test JSON report to the flakiness.io service").argument("<relative-path-to-json>", "Path to the Playwright JSON report file").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePath, options) => runCommand(async () => {
2260
2272
  await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
2261
2273
  }));
2262
- var optLink = new Option("--link <org/proj>", "A project to link to");
2263
- program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
2274
+ program.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
2264
2275
  await cmdLogin(options.endpoint);
2265
- if (options.link)
2266
- await cmdLink(options.link);
2267
2276
  }));
2268
2277
  program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
2269
2278
  await cmdLogout();
@@ -2271,11 +2280,25 @@ program.command("logout").description("Logout from current session").action(asyn
2271
2280
  program.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
2272
2281
  await cmdWhoami();
2273
2282
  }));
2274
- 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 () => {
2275
- const session2 = await FlakinessSession.load();
2276
- if (!session2)
2277
- await cmdLogin(options.endpoint);
2278
- await cmdLink(slug);
2283
+ 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 () => {
2284
+ let slug = slugOrUrl;
2285
+ let endpoint = options.endpoint;
2286
+ if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
2287
+ const url = URL.parse(slugOrUrl);
2288
+ if (!url) {
2289
+ console.error(`Invalid URL: ${slugOrUrl}`);
2290
+ process.exit(1);
2291
+ }
2292
+ slug = url.pathname.substring(1);
2293
+ endpoint = url.origin;
2294
+ } else if (slugOrUrl.startsWith("flakiness.io/")) {
2295
+ endpoint = "https://flakiness.io";
2296
+ slug = slugOrUrl.substring("flakiness.io/".length);
2297
+ }
2298
+ let session2 = await FlakinessSession.load();
2299
+ if (!session2 || session2.endpoint() !== endpoint || await session2.api.user.whoami.GET().catch((e) => void 0) === void 0)
2300
+ session2 = await cmdLogin(endpoint);
2301
+ await cmdLink(session2, slug);
2279
2302
  }));
2280
2303
  program.command("unlink").description("Unlink repository from the flakiness project").action(async () => runCommand(async () => {
2281
2304
  await cmdUnlink();
@@ -2328,16 +2351,16 @@ program.command("download").description("Download run").addOption(optSince).addO
2328
2351
  }
2329
2352
  await Promise.all(downloaders);
2330
2353
  }));
2331
- program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-path>", "Path to the Flakiness report file").addOption(optAccessToken).addOption(optEndpoint).addOption(optAttachmentsDir).action(async (relativePath, options) => {
2354
+ 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) => {
2332
2355
  await runCommand(async () => {
2333
- await cmdUpload(relativePath, await ensureAccessToken(options));
2356
+ await cmdUpload(relativePaths, await ensureAccessToken(options));
2334
2357
  });
2335
2358
  });
2336
- 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 () => {
2359
+ 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 () => {
2337
2360
  const dir = path11.resolve(arg ?? "flakiness-report");
2338
2361
  await cmdShowReport(dir);
2339
2362
  }));
2340
- 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) => {
2363
+ 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) => {
2341
2364
  await runCommand(async () => {
2342
2365
  await cmdConvert(junitPath, options);
2343
2366
  });
@@ -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
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 {
@@ -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({
@@ -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())
@@ -185,77 +185,8 @@ var FlakinessConfig = class _FlakinessConfig {
185
185
  }
186
186
  };
187
187
 
188
- // src/flakinessSession.ts
189
- import fs2 from "fs/promises";
190
- import os from "os";
191
- import path3 from "path";
192
-
193
- // src/serverapi.ts
194
- import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
195
- function createServerAPI(endpoint, options) {
196
- endpoint += "/api/";
197
- const fetcher = options?.auth ? (url, init) => fetch(url, {
198
- ...init,
199
- headers: {
200
- ...init.headers,
201
- "Authorization": `Bearer ${options.auth}`
202
- }
203
- }) : fetch;
204
- if (options?.retries)
205
- return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
206
- return TypedHTTP.createClient(endpoint, fetcher);
207
- }
208
-
209
- // src/flakinessSession.ts
210
- var CONFIG_DIR = (() => {
211
- const configDir = process.platform === "darwin" ? path3.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path3.join(os.homedir(), "AppData", "Roaming", "flakiness") : path3.join(os.homedir(), ".config", "flakiness");
212
- return configDir;
213
- })();
214
- var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
215
- var FlakinessSession = class _FlakinessSession {
216
- constructor(_config) {
217
- this._config = _config;
218
- this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
219
- }
220
- static async loadOrDie() {
221
- const session = await _FlakinessSession.load();
222
- if (!session)
223
- throw new Error(`Please login first with 'npx flakiness login'`);
224
- return session;
225
- }
226
- static async load() {
227
- const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
228
- if (!data)
229
- return void 0;
230
- const json = JSON.parse(data);
231
- return new _FlakinessSession(json);
232
- }
233
- static async remove() {
234
- await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
235
- }
236
- api;
237
- endpoint() {
238
- return this._config.endpoint;
239
- }
240
- path() {
241
- return CONFIG_PATH;
242
- }
243
- sessionToken() {
244
- return this._config.token;
245
- }
246
- async save() {
247
- await fs2.mkdir(CONFIG_DIR, { recursive: true });
248
- await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
249
- }
250
- };
251
-
252
188
  // src/cli/cmd-link.ts
253
- async function cmdLink(slug) {
254
- const session = await FlakinessSession.load();
255
- if (!session) {
256
- console.log(`Please login first`);
257
- process.exit(1);
258
- }
189
+ async function cmdLink(session, slug) {
259
190
  const [orgSlug, projectSlug] = slug.split("/");
260
191
  const project = await session.api.project.findProject.GET({
261
192
  orgSlug,
@@ -171,12 +171,13 @@ async function cmdLogout() {
171
171
  return;
172
172
  const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
173
173
  if (currentSession)
174
- await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
174
+ await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
175
175
  await FlakinessSession.remove();
176
176
  }
177
177
 
178
178
  // src/cli/cmd-login.ts
179
- async function cmdLogin(endpoint) {
179
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
180
+ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
180
181
  await cmdLogout();
181
182
  const api = createServerAPI(endpoint);
182
183
  const data = await api.deviceauth.createRequest.POST({
@@ -190,8 +191,8 @@ async function cmdLogin(endpoint) {
190
191
  await new Promise((x) => setTimeout(x, 2e3));
191
192
  const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
192
193
  if (!result) {
193
- console.log(`Authorization request was rejected.`);
194
- return;
194
+ console.error(`Authorization request was rejected.`);
195
+ process.exit(1);
195
196
  }
196
197
  token = result.token;
197
198
  if (token)
@@ -213,8 +214,10 @@ async function cmdLogin(endpoint) {
213
214
  const message = e instanceof Error ? e.message : String(e);
214
215
  console.error(`x Failed to login:`, message);
215
216
  }
217
+ return session;
216
218
  }
217
219
  export {
220
+ DEFAULT_FLAKINESS_ENDPOINT,
218
221
  cmdLogin
219
222
  };
220
223
  //# sourceMappingURL=cmd-login.js.map
@@ -161,7 +161,7 @@ async function cmdLogout() {
161
161
  return;
162
162
  const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
163
163
  if (currentSession)
164
- await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
164
+ await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
165
165
  await FlakinessSession.remove();
166
166
  }
167
167
  export {
@@ -320,8 +320,10 @@ async function listLocalCommits(gitRoot, head, count) {
320
320
  // %at: Author date as a Unix timestamp (seconds since epoch)
321
321
  "%an",
322
322
  // %an: Author name
323
- "%s"
323
+ "%s",
324
324
  // %s: Subject (the first line of the commit message)
325
+ "%P"
326
+ // %P: Parent hashes (space-separated)
325
327
  ].join(FIELD_SEPARATOR);
326
328
  const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
327
329
  try {
@@ -330,13 +332,14 @@ async function listLocalCommits(gitRoot, head, count) {
330
332
  return [];
331
333
  }
332
334
  return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
333
- const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
335
+ const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
336
+ const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
334
337
  return {
335
338
  commitId,
336
339
  timestamp: parseInt(timestampStr, 10) * 1e3,
337
- // Convert timestamp from seconds to milliseconds
338
340
  author,
339
341
  message,
342
+ parents,
340
343
  walkIndex: 0
341
344
  };
342
345
  });
@@ -288,29 +288,31 @@ var ReportUpload = class {
288
288
  var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
289
289
  var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
290
290
  var log = (txt) => console.log(`[flakiness.io] ${txt}`);
291
- async function cmdUpload(relativePath, options) {
292
- const fullPath = path2.resolve(relativePath);
293
- if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
294
- err(`Path ${fullPath} is not accessible!`);
295
- process.exit(1);
296
- }
297
- const text = await fs3.readFile(fullPath, "utf-8");
298
- const report = JSON.parse(text);
299
- const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
300
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
301
- if (missingAttachments.length) {
302
- warn(`Missing ${missingAttachments.length} attachments`);
303
- }
291
+ async function cmdUpload(relativePaths, options) {
304
292
  const uploader = new ReportUploader({
305
293
  flakinessAccessToken: options.accessToken,
306
294
  flakinessEndpoint: options.endpoint
307
295
  });
308
- const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
309
- const uploadResult = await upload.upload();
310
- if (!uploadResult.success) {
311
- err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
312
- } else {
313
- log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
296
+ for (const relativePath of relativePaths) {
297
+ const fullPath = path2.resolve(relativePath);
298
+ if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
299
+ err(`Path ${fullPath} is not accessible!`);
300
+ process.exit(1);
301
+ }
302
+ const text = await fs3.readFile(fullPath, "utf-8");
303
+ const report = JSON.parse(text);
304
+ const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
305
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
306
+ if (missingAttachments.length) {
307
+ warn(`Missing ${missingAttachments.length} attachments`);
308
+ }
309
+ const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
310
+ const uploadResult = await upload.upload();
311
+ if (!uploadResult.success) {
312
+ err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
313
+ } else {
314
+ log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
315
+ }
314
316
  }
315
317
  }
316
318
  export {
package/lib/localGit.js CHANGED
@@ -12,8 +12,10 @@ async function listLocalCommits(gitRoot, head, count) {
12
12
  // %at: Author date as a Unix timestamp (seconds since epoch)
13
13
  "%an",
14
14
  // %an: Author name
15
- "%s"
15
+ "%s",
16
16
  // %s: Subject (the first line of the commit message)
17
+ "%P"
18
+ // %P: Parent hashes (space-separated)
17
19
  ].join(FIELD_SEPARATOR);
18
20
  const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
19
21
  try {
@@ -22,13 +24,14 @@ async function listLocalCommits(gitRoot, head, count) {
22
24
  return [];
23
25
  }
24
26
  return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
25
- const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
27
+ const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
28
+ const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
26
29
  return {
27
30
  commitId,
28
31
  timestamp: parseInt(timestampStr, 10) * 1e3,
29
- // Convert timestamp from seconds to milliseconds
30
32
  author,
31
33
  message,
34
+ parents,
32
35
  walkIndex: 0
33
36
  };
34
37
  });
@@ -18,8 +18,10 @@ async function listLocalCommits(gitRoot, head, count) {
18
18
  // %at: Author date as a Unix timestamp (seconds since epoch)
19
19
  "%an",
20
20
  // %an: Author name
21
- "%s"
21
+ "%s",
22
22
  // %s: Subject (the first line of the commit message)
23
+ "%P"
24
+ // %P: Parent hashes (space-separated)
23
25
  ].join(FIELD_SEPARATOR);
24
26
  const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
25
27
  try {
@@ -28,13 +30,14 @@ async function listLocalCommits(gitRoot, head, count) {
28
30
  return [];
29
31
  }
30
32
  return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
31
- const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
33
+ const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
34
+ const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
32
35
  return {
33
36
  commitId,
34
37
  timestamp: parseInt(timestampStr, 10) * 1e3,
35
- // Convert timestamp from seconds to milliseconds
36
38
  author,
37
39
  message,
40
+ parents,
38
41
  walkIndex: 0
39
42
  };
40
43
  });