@flakiness/sdk 0.95.0 → 0.97.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
@@ -1,238 +1,751 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli/cli.ts
4
- import { Command, Option } from "commander";
5
- import fs10 from "fs";
6
- import path8 from "path";
7
-
8
- // src/flakinessSession.ts
9
- import fs2 from "fs/promises";
10
- import os2 from "os";
11
- import path from "path";
12
-
13
- // ../server/lib/common/typedHttp.js
14
- var TypedHTTP;
15
- ((TypedHTTP2) => {
16
- TypedHTTP2.StatusCodes = {
17
- Informational: {
18
- CONTINUE: 100,
19
- SWITCHING_PROTOCOLS: 101,
20
- PROCESSING: 102,
21
- EARLY_HINTS: 103
22
- },
23
- Success: {
24
- OK: 200,
25
- CREATED: 201,
26
- ACCEPTED: 202,
27
- NON_AUTHORITATIVE_INFORMATION: 203,
28
- NO_CONTENT: 204,
29
- RESET_CONTENT: 205,
30
- PARTIAL_CONTENT: 206,
31
- MULTI_STATUS: 207
32
- },
33
- Redirection: {
34
- MULTIPLE_CHOICES: 300,
35
- MOVED_PERMANENTLY: 301,
36
- MOVED_TEMPORARILY: 302,
37
- SEE_OTHER: 303,
38
- NOT_MODIFIED: 304,
39
- USE_PROXY: 305,
40
- TEMPORARY_REDIRECT: 307,
41
- PERMANENT_REDIRECT: 308
42
- },
43
- ClientErrors: {
44
- BAD_REQUEST: 400,
45
- UNAUTHORIZED: 401,
46
- PAYMENT_REQUIRED: 402,
47
- FORBIDDEN: 403,
48
- NOT_FOUND: 404,
49
- METHOD_NOT_ALLOWED: 405,
50
- NOT_ACCEPTABLE: 406,
51
- PROXY_AUTHENTICATION_REQUIRED: 407,
52
- REQUEST_TIMEOUT: 408,
53
- CONFLICT: 409,
54
- GONE: 410,
55
- LENGTH_REQUIRED: 411,
56
- PRECONDITION_FAILED: 412,
57
- REQUEST_TOO_LONG: 413,
58
- REQUEST_URI_TOO_LONG: 414,
59
- UNSUPPORTED_MEDIA_TYPE: 415,
60
- REQUESTED_RANGE_NOT_SATISFIABLE: 416,
61
- EXPECTATION_FAILED: 417,
62
- IM_A_TEAPOT: 418,
63
- INSUFFICIENT_SPACE_ON_RESOURCE: 419,
64
- METHOD_FAILURE: 420,
65
- MISDIRECTED_REQUEST: 421,
66
- UNPROCESSABLE_ENTITY: 422,
67
- LOCKED: 423,
68
- FAILED_DEPENDENCY: 424,
69
- UPGRADE_REQUIRED: 426,
70
- PRECONDITION_REQUIRED: 428,
71
- TOO_MANY_REQUESTS: 429,
72
- REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
73
- UNAVAILABLE_FOR_LEGAL_REASONS: 451
74
- },
75
- ServerErrors: {
76
- INTERNAL_SERVER_ERROR: 500,
77
- NOT_IMPLEMENTED: 501,
78
- BAD_GATEWAY: 502,
79
- SERVICE_UNAVAILABLE: 503,
80
- GATEWAY_TIMEOUT: 504,
81
- HTTP_VERSION_NOT_SUPPORTED: 505,
82
- INSUFFICIENT_STORAGE: 507,
83
- NETWORK_AUTHENTICATION_REQUIRED: 511
84
- }
85
- };
86
- const AllErrorCodes = {
87
- ...TypedHTTP2.StatusCodes.ClientErrors,
88
- ...TypedHTTP2.StatusCodes.ServerErrors
89
- };
90
- class HttpError extends Error {
91
- constructor(status, message) {
92
- super(message);
93
- this.status = status;
3
+ // ../node_modules/vlq/src/index.js
4
+ var char_to_integer = {};
5
+ var integer_to_char = {};
6
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
7
+ char_to_integer[char] = i;
8
+ integer_to_char[i] = char;
9
+ });
10
+ function decode(string) {
11
+ let result = [];
12
+ let shift = 0;
13
+ let value = 0;
14
+ for (let i = 0; i < string.length; i += 1) {
15
+ let integer = char_to_integer[string[i]];
16
+ if (integer === void 0) {
17
+ throw new Error("Invalid character (" + string[i] + ")");
94
18
  }
95
- static withCode(code, message) {
96
- const statusCode = AllErrorCodes[code];
97
- const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
98
- return new HttpError(statusCode, message ?? defaultMessage);
19
+ const has_continuation_bit = integer & 32;
20
+ integer &= 31;
21
+ value += integer << shift;
22
+ if (has_continuation_bit) {
23
+ shift += 5;
24
+ } else {
25
+ const should_negate = value & 1;
26
+ value >>>= 1;
27
+ if (should_negate) {
28
+ result.push(value === 0 ? -2147483648 : -value);
29
+ } else {
30
+ result.push(value);
31
+ }
32
+ value = shift = 0;
99
33
  }
100
34
  }
101
- TypedHTTP2.HttpError = HttpError;
102
- function isInformationalResponse(response) {
103
- return response.status >= 100 && response.status < 200;
35
+ return result;
36
+ }
37
+ function encode(value) {
38
+ if (typeof value === "number") {
39
+ return encode_integer(value);
104
40
  }
105
- TypedHTTP2.isInformationalResponse = isInformationalResponse;
106
- function isSuccessResponse(response) {
107
- return response.status >= 200 && response.status < 300;
41
+ let result = "";
42
+ for (let i = 0; i < value.length; i += 1) {
43
+ result += encode_integer(value[i]);
108
44
  }
109
- TypedHTTP2.isSuccessResponse = isSuccessResponse;
110
- function isRedirectResponse(response) {
111
- return response.status >= 300 && response.status < 400;
45
+ return result;
46
+ }
47
+ function encode_integer(num) {
48
+ let result = "";
49
+ if (num < 0) {
50
+ num = -num << 1 | 1;
51
+ } else {
52
+ num <<= 1;
112
53
  }
113
- TypedHTTP2.isRedirectResponse = isRedirectResponse;
114
- function isErrorResponse(response) {
115
- return response.status >= 400 && response.status < 600;
54
+ do {
55
+ let clamped = num & 31;
56
+ num >>>= 5;
57
+ if (num > 0) {
58
+ clamped |= 32;
59
+ }
60
+ result += integer_to_char[clamped];
61
+ } while (num > 0);
62
+ return result;
63
+ }
64
+
65
+ // ../server/lib/common/heap.js
66
+ var Heap = class _Heap {
67
+ constructor(_cmp, elements = []) {
68
+ this._cmp = _cmp;
69
+ this._heap = elements.map(([element, score]) => ({ element, score }));
70
+ for (let idx = this._heap.length - 1; idx >= 0; --idx)
71
+ this._down(idx);
72
+ }
73
+ static createMin(elements = []) {
74
+ return new _Heap((a, b) => a - b, elements);
75
+ }
76
+ static createMax(elements = []) {
77
+ return new _Heap((a, b) => b - a, elements);
78
+ }
79
+ _heap = [];
80
+ _up(idx) {
81
+ const e = this._heap[idx];
82
+ while (idx > 0) {
83
+ const parentIdx = idx - 1 >>> 1;
84
+ if (this._cmp(this._heap[parentIdx].score, e.score) <= 0)
85
+ break;
86
+ this._heap[idx] = this._heap[parentIdx];
87
+ idx = parentIdx;
88
+ }
89
+ this._heap[idx] = e;
90
+ }
91
+ _down(idx) {
92
+ const N = this._heap.length;
93
+ const e = this._heap[idx];
94
+ while (true) {
95
+ const leftIdx = idx * 2 + 1;
96
+ const rightIdx = idx * 2 + 2;
97
+ let smallestIdx;
98
+ if (leftIdx < N && rightIdx < N) {
99
+ smallestIdx = this._cmp(this._heap[leftIdx].score, this._heap[rightIdx].score) < 0 ? leftIdx : rightIdx;
100
+ } else if (leftIdx < N) {
101
+ smallestIdx = leftIdx;
102
+ } else if (rightIdx < N) {
103
+ smallestIdx = rightIdx;
104
+ } else {
105
+ break;
106
+ }
107
+ if (this._cmp(e.score, this._heap[smallestIdx].score) < 0)
108
+ break;
109
+ this._heap[idx] = this._heap[smallestIdx];
110
+ idx = smallestIdx;
111
+ }
112
+ this._heap[idx] = e;
116
113
  }
117
- TypedHTTP2.isErrorResponse = isErrorResponse;
118
- function info(status) {
119
- return { status };
114
+ push(element, score) {
115
+ this._heap.push({ element, score });
116
+ this._up(this._heap.length - 1);
120
117
  }
121
- TypedHTTP2.info = info;
122
- function ok(data, status) {
123
- return {
124
- status: status ?? TypedHTTP2.StatusCodes.Success.OK,
125
- data
126
- };
118
+ get size() {
119
+ return this._heap.length;
127
120
  }
128
- TypedHTTP2.ok = ok;
129
- function redirect(url, status = 302) {
130
- return { status, url };
121
+ peekEntry() {
122
+ return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
131
123
  }
132
- TypedHTTP2.redirect = redirect;
133
- function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
134
- return { status, message };
124
+ popEntry() {
125
+ if (!this._heap.length)
126
+ return void 0;
127
+ const entry = this._heap[0];
128
+ const last = this._heap.pop();
129
+ if (!this._heap.length)
130
+ return [entry.element, entry.score];
131
+ this._heap[0] = last;
132
+ this._down(0);
133
+ return [entry.element, entry.score];
135
134
  }
136
- TypedHTTP2.error = error;
137
- class Router {
138
- constructor(_resolveContext) {
139
- this._resolveContext = _resolveContext;
135
+ };
136
+
137
+ // ../server/lib/common/sequence.js
138
+ var Sequence = class _Sequence {
139
+ constructor(_seek, length) {
140
+ this._seek = _seek;
141
+ this.length = length;
142
+ }
143
+ static fromList(a) {
144
+ return new _Sequence(
145
+ function(pos) {
146
+ return {
147
+ next() {
148
+ if (pos >= a.length)
149
+ return { done: true, value: void 0 };
150
+ return { done: false, value: a[pos++] };
151
+ }
152
+ };
153
+ },
154
+ a.length
155
+ );
156
+ }
157
+ static chain(seqs) {
158
+ const leftsums = [];
159
+ let length = 0;
160
+ for (let i = 0; i < seqs.length; ++i) {
161
+ length += seqs[i].length;
162
+ leftsums.push(length);
140
163
  }
141
- static create() {
142
- return new Router(async (e) => e.ctx);
164
+ return new _Sequence(
165
+ function(fromIdx) {
166
+ fromIdx = Math.max(0, Math.min(length, fromIdx));
167
+ let idx = _Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
168
+ if (idx >= seqs.length) {
169
+ return {
170
+ next: () => ({ done: true, value: void 0 })
171
+ };
172
+ }
173
+ let it = seqs[idx].seek(idx > 0 ? fromIdx - leftsums[idx - 1] : fromIdx);
174
+ return {
175
+ next() {
176
+ let result = it.next();
177
+ while (result.done && ++idx < seqs.length) {
178
+ it = seqs[idx].seek(0);
179
+ result = it.next();
180
+ }
181
+ return result.done ? result : { done: false, value: [result.value, idx] };
182
+ }
183
+ };
184
+ },
185
+ length
186
+ );
187
+ }
188
+ static merge(sequences, cmp) {
189
+ const length = sequences.reduce((acc, seq) => acc + seq.length, 0);
190
+ return new _Sequence(
191
+ function(fromIdx) {
192
+ fromIdx = Math.max(0, Math.min(length, fromIdx));
193
+ const offsets = quickAdvance(sequences, cmp, fromIdx);
194
+ const entries = [];
195
+ for (let i = 0; i < sequences.length; ++i) {
196
+ const seq = sequences[i];
197
+ const it = seq.seek(offsets[i]);
198
+ const itval = it.next();
199
+ if (!itval.done)
200
+ entries.push([it, itval.value]);
201
+ }
202
+ const heap = new Heap(cmp, entries);
203
+ return {
204
+ next() {
205
+ if (!heap.size)
206
+ return { done: true, value: void 0 };
207
+ ++fromIdx;
208
+ const [it, e] = heap.popEntry();
209
+ const itval = it.next();
210
+ if (!itval.done)
211
+ heap.push(it, itval.value);
212
+ return { done: false, value: e };
213
+ }
214
+ };
215
+ },
216
+ length
217
+ );
218
+ }
219
+ static EMPTY = new _Sequence(function* () {
220
+ }, 0);
221
+ seek(idx) {
222
+ const it = this._seek(idx);
223
+ it[Symbol.iterator] = () => it;
224
+ return it;
225
+ }
226
+ get(idx) {
227
+ return this.seek(idx).next().value;
228
+ }
229
+ map(mapper) {
230
+ const originalSeek = this._seek;
231
+ return new _Sequence(
232
+ function(idx) {
233
+ const it = originalSeek(idx);
234
+ return {
235
+ next() {
236
+ const next = it.next();
237
+ if (next.done)
238
+ return next;
239
+ return { done: false, value: mapper(next.value) };
240
+ }
241
+ };
242
+ },
243
+ this.length
244
+ );
245
+ }
246
+ /** Number of elements in sequence that are <= comparator. Only works on sorted sequences. */
247
+ partitionPoint(predicate) {
248
+ let lo = 0, hi = this.length;
249
+ while (lo < hi) {
250
+ const mid = lo + hi >>> 1;
251
+ if (predicate(this.get(mid)))
252
+ lo = mid + 1;
253
+ else
254
+ hi = mid;
143
255
  }
144
- rawMethod(method, route) {
145
- return {
146
- [method]: {
147
- method,
148
- input: route.input,
149
- etag: route.etag,
150
- resolveContext: this._resolveContext,
151
- handler: route.handler
256
+ return lo;
257
+ }
258
+ };
259
+ function quickAdvance(sequences, cmp, k) {
260
+ const offsets = new Map(sequences.map((s) => [s, 0]));
261
+ while (offsets.size && k > 0) {
262
+ const t2 = offsets.size;
263
+ const x = Math.max(Math.floor(k / t2 / 2), 1);
264
+ const entries = [];
265
+ for (const [seq, offset] of offsets) {
266
+ if (offset + x <= seq.length)
267
+ entries.push([seq, seq.get(offset + x - 1)]);
268
+ }
269
+ const heap = new Heap(cmp, entries);
270
+ while (heap.size && k > 0 && (x === 1 || k >= x * t2)) {
271
+ k -= x;
272
+ const [seq] = heap.popEntry();
273
+ const offset = offsets.get(seq) + x;
274
+ if (offset === seq.length)
275
+ offsets.delete(seq);
276
+ else
277
+ offsets.set(seq, offset);
278
+ if (offset + x <= seq.length)
279
+ heap.push(seq, seq.get(offset + x - 1));
280
+ }
281
+ }
282
+ return sequences.map((seq) => offsets.get(seq) ?? seq.length);
283
+ }
284
+
285
+ // ../server/lib/common/ranges.js
286
+ var Ranges;
287
+ ((Ranges2) => {
288
+ Ranges2.EMPTY = [];
289
+ Ranges2.FULL = [-Infinity, Infinity];
290
+ function isFull(ranges) {
291
+ return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
292
+ }
293
+ Ranges2.isFull = isFull;
294
+ function compress(ranges) {
295
+ if (!ranges.length)
296
+ return "";
297
+ if (isInfinite(ranges))
298
+ throw new Error("Compression of infinite ranges is not supported");
299
+ const prepared = [];
300
+ let last = ranges[0] - 1;
301
+ prepared.push(last);
302
+ for (let i = 0; i < ranges.length; i += 2) {
303
+ if (ranges[i] === ranges[i + 1]) {
304
+ prepared.push(-(ranges[i] - last));
305
+ } else {
306
+ prepared.push(ranges[i] - last);
307
+ prepared.push(ranges[i + 1] - ranges[i]);
308
+ }
309
+ last = ranges[i + 1];
310
+ }
311
+ return encode(prepared);
312
+ }
313
+ Ranges2.compress = compress;
314
+ function decompress(compressed) {
315
+ if (!compressed.length)
316
+ return [];
317
+ const prepared = decode(compressed);
318
+ const result = [];
319
+ let last = prepared[0];
320
+ for (let i = 1; i < prepared.length; ++i) {
321
+ if (prepared[i] < 0) {
322
+ result.push(-prepared[i] + last);
323
+ result.push(-prepared[i] + last);
324
+ last -= prepared[i];
325
+ } else {
326
+ result.push(prepared[i] + last);
327
+ last += prepared[i];
328
+ }
329
+ }
330
+ return result;
331
+ }
332
+ Ranges2.decompress = decompress;
333
+ function toString(ranges) {
334
+ const tokens = [];
335
+ for (let i = 0; i < ranges.length - 1; i += 2) {
336
+ if (ranges[i] === ranges[i + 1])
337
+ tokens.push(ranges[i]);
338
+ else
339
+ tokens.push(`${ranges[i]}-${ranges[i + 1]}`);
340
+ }
341
+ if (!tokens.length)
342
+ return `[]`;
343
+ return `[ ` + tokens.join(", ") + ` ]`;
344
+ }
345
+ Ranges2.toString = toString;
346
+ function popInplace(ranges) {
347
+ if (isInfinite(ranges))
348
+ throw new Error("cannot pop from infinite ranges!");
349
+ const last = ranges.at(-1);
350
+ const prelast = ranges.at(-2);
351
+ if (last === void 0 || prelast === void 0)
352
+ return void 0;
353
+ if (last === prelast) {
354
+ ranges.pop();
355
+ ranges.pop();
356
+ } else {
357
+ ranges[ranges.length - 1] = last - 1;
358
+ }
359
+ return last;
360
+ }
361
+ Ranges2.popInplace = popInplace;
362
+ function* iterate(ranges) {
363
+ if (isInfinite(ranges))
364
+ throw new Error("cannot iterate infinite ranges!");
365
+ for (let i = 0; i < ranges.length - 1; i += 2) {
366
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
367
+ yield j;
368
+ }
369
+ }
370
+ Ranges2.iterate = iterate;
371
+ function toSortedList(ranges) {
372
+ if (isInfinite(ranges))
373
+ throw new Error("cannot convert infinite ranges!");
374
+ const list = [];
375
+ for (let i = 0; i < ranges.length - 1; i += 2) {
376
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
377
+ list.push(j);
378
+ }
379
+ return list;
380
+ }
381
+ Ranges2.toSortedList = toSortedList;
382
+ function toInt32Array(ranges) {
383
+ if (isInfinite(ranges))
384
+ throw new Error("cannot convert infinite ranges!");
385
+ const result = new Int32Array(cardinality(ranges));
386
+ let idx = 0;
387
+ for (let i = 0; i < ranges.length - 1; i += 2) {
388
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
389
+ result[idx++] = j;
390
+ }
391
+ return result;
392
+ }
393
+ Ranges2.toInt32Array = toInt32Array;
394
+ function fromList(x) {
395
+ for (let i = 0; i < x.length - 1; ++i) {
396
+ if (x[i] > x[i + 1]) {
397
+ x = x.toSorted((a, b) => a - b);
398
+ break;
399
+ }
400
+ }
401
+ return fromSortedList(x);
402
+ }
403
+ Ranges2.fromList = fromList;
404
+ function from(x) {
405
+ return [x, x];
406
+ }
407
+ Ranges2.from = from;
408
+ function fromSortedList(sorted) {
409
+ const ranges = [];
410
+ let rangeStart = 0;
411
+ for (let i = 1; i <= sorted.length; ++i) {
412
+ if (i < sorted.length && sorted[i] - sorted[i - 1] <= 1)
413
+ continue;
414
+ ranges.push(sorted[rangeStart], sorted[i - 1]);
415
+ rangeStart = i;
416
+ }
417
+ return ranges;
418
+ }
419
+ Ranges2.fromSortedList = fromSortedList;
420
+ function isInfinite(ranges) {
421
+ return ranges.length > 0 && (Object.is(ranges[0], Infinity) || Object.is(ranges[ranges.length - 1], Infinity));
422
+ }
423
+ Ranges2.isInfinite = isInfinite;
424
+ function includes(ranges, e) {
425
+ if (!ranges.length)
426
+ return false;
427
+ if (e < ranges[0] || ranges[ranges.length - 1] < e)
428
+ return false;
429
+ if (ranges.length < 17) {
430
+ for (let i = 0; i < ranges.length - 1; i += 2) {
431
+ if (ranges[i] <= e && e <= ranges[i + 1])
432
+ return true;
433
+ }
434
+ return false;
435
+ }
436
+ let lo = 0, hi = ranges.length;
437
+ while (lo < hi) {
438
+ const mid = lo + hi >>> 1;
439
+ if (ranges[mid] === e)
440
+ return true;
441
+ if (ranges[mid] < e)
442
+ lo = mid + 1;
443
+ else
444
+ hi = mid;
445
+ }
446
+ return (lo & 1) !== 0;
447
+ }
448
+ Ranges2.includes = includes;
449
+ function cardinality(ranges) {
450
+ if (isInfinite(ranges))
451
+ return Infinity;
452
+ let sum = 0;
453
+ for (let i = 0; i < ranges.length - 1; i += 2)
454
+ sum += ranges[i + 1] - ranges[i] + 1;
455
+ return sum;
456
+ }
457
+ Ranges2.cardinality = cardinality;
458
+ function offset(ranges, offset2) {
459
+ return ranges.map((x) => x + offset2);
460
+ }
461
+ Ranges2.offset = offset;
462
+ function intersect(ranges1, ranges2) {
463
+ const ranges = [];
464
+ if (!ranges1.length || !ranges2.length)
465
+ return ranges;
466
+ if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
467
+ return ranges;
468
+ let p1 = 0;
469
+ let p2 = 0;
470
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
471
+ if (ranges1[p1 + 1] < ranges2[p2]) {
472
+ p1 += 2;
473
+ let offset2 = 1;
474
+ while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
475
+ offset2 <<= 1;
476
+ p1 += offset2 >> 1 << 1;
477
+ } else if (ranges2[p2 + 1] < ranges1[p1]) {
478
+ p2 += 2;
479
+ let offset2 = 1;
480
+ while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
481
+ offset2 <<= 1;
482
+ p2 += offset2 >> 1 << 1;
483
+ } else {
484
+ const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
485
+ const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
486
+ ranges.push(Math.max(a1, b1), Math.min(a2, b2));
487
+ if (a2 < b2) {
488
+ p1 += 2;
489
+ } else if (a2 > b2) {
490
+ p2 += 2;
491
+ } else {
492
+ p1 += 2;
493
+ p2 += 2;
152
494
  }
153
- };
495
+ }
154
496
  }
155
- get(route) {
156
- return this.rawMethod("GET", {
157
- ...route,
158
- handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
159
- });
497
+ return ranges;
498
+ }
499
+ Ranges2.intersect = intersect;
500
+ function capAt(ranges, cap) {
501
+ const result = [];
502
+ for (let i = 0; i < ranges.length; i += 2) {
503
+ const start = ranges[i];
504
+ const end = ranges[i + 1];
505
+ if (start > cap)
506
+ break;
507
+ if (end <= cap) {
508
+ result.push(start, end);
509
+ } else {
510
+ result.push(start, cap);
511
+ break;
512
+ }
160
513
  }
161
- post(route) {
162
- return this.rawMethod("POST", {
163
- ...route,
164
- handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
165
- });
514
+ return result;
515
+ }
516
+ Ranges2.capAt = capAt;
517
+ function isIntersecting(ranges1, ranges2) {
518
+ if (!ranges1.length || !ranges2.length)
519
+ return false;
520
+ if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
521
+ return false;
522
+ let p1 = 0;
523
+ let p2 = 0;
524
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
525
+ const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
526
+ const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
527
+ if (a2 < b1) {
528
+ p1 += 2;
529
+ let offset2 = 1;
530
+ while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
531
+ offset2 <<= 1;
532
+ p1 += offset2 >> 1 << 1;
533
+ } else if (b2 < a1) {
534
+ p2 += 2;
535
+ let offset2 = 1;
536
+ while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
537
+ offset2 <<= 1;
538
+ p2 += offset2 >> 1 << 1;
539
+ } else {
540
+ return true;
541
+ }
166
542
  }
167
- use(resolveContext) {
168
- return new Router(async (options) => {
169
- const m = await this._resolveContext(options);
170
- return resolveContext({ ...options, ctx: m });
171
- });
543
+ return false;
544
+ }
545
+ Ranges2.isIntersecting = isIntersecting;
546
+ function complement(r) {
547
+ if (r.length === 0)
548
+ return [-Infinity, Infinity];
549
+ const result = [];
550
+ if (!Object.is(r[0], -Infinity))
551
+ result.push(-Infinity, r[0] - 1);
552
+ for (let i = 1; i < r.length - 2; i += 2)
553
+ result.push(r[i] + 1, r[i + 1] - 1);
554
+ if (!Object.is(r[r.length - 1], Infinity))
555
+ result.push(r[r.length - 1] + 1, Infinity);
556
+ return result;
557
+ }
558
+ Ranges2.complement = complement;
559
+ function subtract(ranges1, ranges2) {
560
+ return intersect(ranges1, complement(ranges2));
561
+ }
562
+ Ranges2.subtract = subtract;
563
+ function singleRange(from2, to) {
564
+ return [from2, to];
565
+ }
566
+ Ranges2.singleRange = singleRange;
567
+ function unionAll(ranges) {
568
+ let result = Ranges2.EMPTY;
569
+ for (const r of ranges)
570
+ result = union(result, r);
571
+ return result;
572
+ }
573
+ Ranges2.unionAll = unionAll;
574
+ function unionAll_2(rangesIterable) {
575
+ const ranges = Array.isArray(rangesIterable) ? rangesIterable : Array.from(rangesIterable);
576
+ if (ranges.length === 0)
577
+ return [];
578
+ if (ranges.length === 1)
579
+ return ranges[0];
580
+ if (ranges.length === 2)
581
+ return union(ranges[0], ranges[1]);
582
+ const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
583
+ const result = [];
584
+ let last;
585
+ for (const interval of seq.seek(0)) {
586
+ if (!last || last[1] + 1 < interval[0]) {
587
+ result.push(interval);
588
+ last = interval;
589
+ continue;
590
+ }
591
+ if (last[1] < interval[1])
592
+ last[1] = interval[1];
593
+ }
594
+ return result.flat();
595
+ }
596
+ Ranges2.unionAll_2 = unionAll_2;
597
+ function intersectAll(ranges) {
598
+ if (!ranges.length)
599
+ return Ranges2.EMPTY;
600
+ let result = Ranges2.FULL;
601
+ for (const range of ranges)
602
+ result = Ranges2.intersect(result, range);
603
+ return result;
604
+ }
605
+ Ranges2.intersectAll = intersectAll;
606
+ function domain(ranges) {
607
+ if (!ranges.length)
608
+ return void 0;
609
+ return { min: ranges[0], max: ranges[ranges.length - 1] };
610
+ }
611
+ Ranges2.domain = domain;
612
+ function union(ranges1, ranges2) {
613
+ if (!ranges1.length)
614
+ return ranges2;
615
+ if (!ranges2.length)
616
+ return ranges1;
617
+ if (ranges2[0] < ranges1[0])
618
+ [ranges1, ranges2] = [ranges2, ranges1];
619
+ const r = [ranges1[0], ranges1[1]];
620
+ let p1 = 2, p2 = 0;
621
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
622
+ if (ranges1[p1] <= ranges2[p2]) {
623
+ if (r[r.length - 1] + 1 < ranges1[p1]) {
624
+ r.push(ranges1[p1], ranges1[p1 + 1]);
625
+ p1 += 2;
626
+ } else if (r[r.length - 1] < ranges1[p1 + 1]) {
627
+ r[r.length - 1] = ranges1[p1 + 1];
628
+ p1 += 2;
629
+ } else {
630
+ p1 += 2;
631
+ let offset2 = 1;
632
+ while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
633
+ offset2 <<= 1;
634
+ p1 += offset2 >> 1 << 1;
635
+ }
636
+ } else {
637
+ if (r[r.length - 1] + 1 < ranges2[p2]) {
638
+ r.push(ranges2[p2], ranges2[p2 + 1]);
639
+ p2 += 2;
640
+ } else if (r[r.length - 1] < ranges2[p2 + 1]) {
641
+ r[r.length - 1] = ranges2[p2 + 1];
642
+ p2 += 2;
643
+ } else {
644
+ p2 += 2;
645
+ let offset2 = 1;
646
+ while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
647
+ offset2 <<= 1;
648
+ p2 += offset2 >> 1 << 1;
649
+ }
650
+ }
172
651
  }
652
+ while (p1 < ranges1.length - 1) {
653
+ if (r[r.length - 1] + 1 < ranges1[p1]) {
654
+ r.push(ranges1[p1], ranges1[p1 + 1]);
655
+ p1 += 2;
656
+ } else if (r[r.length - 1] < ranges1[p1 + 1]) {
657
+ r[r.length - 1] = ranges1[p1 + 1];
658
+ p1 += 2;
659
+ } else {
660
+ p1 += 2;
661
+ let offset2 = 1;
662
+ while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
663
+ offset2 <<= 1;
664
+ p1 += offset2 >> 1 << 1;
665
+ }
666
+ }
667
+ while (p2 < ranges2.length - 1) {
668
+ if (r[r.length - 1] + 1 < ranges2[p2]) {
669
+ r.push(ranges2[p2], ranges2[p2 + 1]);
670
+ p2 += 2;
671
+ } else if (r[r.length - 1] < ranges2[p2 + 1]) {
672
+ r[r.length - 1] = ranges2[p2 + 1];
673
+ p2 += 2;
674
+ } else {
675
+ p2 += 2;
676
+ let offset2 = 1;
677
+ while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
678
+ offset2 <<= 1;
679
+ p2 += offset2 >> 1 << 1;
680
+ }
681
+ }
682
+ return r;
173
683
  }
174
- TypedHTTP2.Router = Router;
175
- function createClient(base, fetchCallback) {
176
- function buildUrl(path9, input, options) {
177
- const method = path9.at(-1);
178
- const url = new URL(path9.slice(0, path9.length - 1).join("/"), base);
179
- const signal = options?.signal;
180
- let body = void 0;
181
- if (method === "GET" && input)
182
- url.searchParams.set("input", JSON.stringify(input));
183
- else if (method !== "GET" && input)
184
- body = JSON.stringify(input);
684
+ Ranges2.union = union;
685
+ function intervalSequence(ranges) {
686
+ return new Sequence(function(idx) {
185
687
  return {
186
- url,
187
- method,
188
- headers: body ? { "Content-Type": "application/json" } : void 0,
189
- body,
190
- signal
688
+ next() {
689
+ if (idx * 2 >= ranges.length)
690
+ return { done: true, value: void 0 };
691
+ const value = [ranges[idx * 2], ranges[idx * 2 + 1]];
692
+ ++idx;
693
+ return { done: false, value };
694
+ }
191
695
  };
696
+ }, ranges.length >>> 1);
697
+ }
698
+ Ranges2.intervalSequence = intervalSequence;
699
+ function sequence(ranges) {
700
+ let length = 0;
701
+ const leftsums = [];
702
+ for (let i = 0; i < ranges.length - 1; i += 2) {
703
+ length += ranges[i + 1] - ranges[i] + 1;
704
+ leftsums.push(length);
192
705
  }
193
- function createProxy(path9 = []) {
194
- return new Proxy(() => {
195
- }, {
196
- get(target, prop) {
197
- if (typeof prop === "symbol")
198
- return void 0;
199
- if (prop === "prepare")
200
- return (input, options) => buildUrl(path9, input, options);
201
- const newPath = [...path9, prop];
202
- return createProxy(newPath);
203
- },
204
- apply(target, thisArg, args) {
205
- const options = buildUrl(path9, args[0], args[1]);
206
- return fetchCallback(options.url, {
207
- method: options.method,
208
- body: options.body,
209
- headers: options.headers,
210
- signal: options.signal
211
- }).then(async (response) => {
212
- if (response.status >= 200 && response.status < 300) {
213
- if (response.headers.get("content-type")?.includes("application/json")) {
214
- const text = await response.text();
215
- return text.length ? JSON.parse(text) : void 0;
216
- }
217
- return await response.blob();
706
+ return new Sequence(
707
+ function(fromIdx) {
708
+ fromIdx = Math.max(0, Math.min(length, fromIdx));
709
+ const idx = Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
710
+ const intervals = Ranges2.intervalSequence(ranges);
711
+ const it = intervals.seek(idx);
712
+ const firstInterval = it.next();
713
+ if (firstInterval.done)
714
+ return { next: () => firstInterval };
715
+ let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
716
+ let to = firstInterval.value[1];
717
+ return {
718
+ next() {
719
+ if (from2 > to) {
720
+ const interval = it.next();
721
+ if (interval.done)
722
+ return { done: true, value: void 0 };
723
+ from2 = interval.value[0];
724
+ to = interval.value[1];
218
725
  }
219
- if (response.status >= 400 && response.status < 600) {
220
- const text = await response.text();
221
- if (text)
222
- throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
223
- else
224
- throw new Error(`HTTP request failed with status ${response.status}`);
225
- }
226
- });
227
- }
228
- });
229
- }
230
- return createProxy();
726
+ return { done: false, value: from2++ };
727
+ }
728
+ };
729
+ },
730
+ length
731
+ );
231
732
  }
232
- TypedHTTP2.createClient = createClient;
233
- })(TypedHTTP || (TypedHTTP = {}));
733
+ Ranges2.sequence = sequence;
734
+ })(Ranges || (Ranges = {}));
735
+
736
+ // src/cli/cli.ts
737
+ import { TypedHTTP as TypedHTTP4 } from "@flakiness/shared/common/typedHttp.js";
738
+ import assert4 from "assert";
739
+ import { Command, Option } from "commander";
740
+ import fs12 from "fs";
741
+ import path10 from "path";
742
+
743
+ // src/flakinessConfig.ts
744
+ import fs2 from "fs";
745
+ import path2 from "path";
234
746
 
235
747
  // src/utils.ts
748
+ import { FlakinessReport } from "@flakiness/report";
236
749
  import assert from "assert";
237
750
  import { spawnSync } from "child_process";
238
751
  import crypto from "crypto";
@@ -240,14 +753,7 @@ import fs from "fs";
240
753
  import http from "http";
241
754
  import https from "https";
242
755
  import os from "os";
243
- import { posix as posixPath, win32 as win32Path } from "path";
244
- import util from "util";
245
- import zlib from "zlib";
246
- var gzipAsync = util.promisify(zlib.gzip);
247
- var gunzipAsync = util.promisify(zlib.gunzip);
248
- var gunzipSync = zlib.gunzipSync;
249
- var brotliCompressAsync = util.promisify(zlib.brotliCompress);
250
- var brotliCompressSync = zlib.brotliCompressSync;
756
+ import path, { posix as posixPath, win32 as win32Path } from "path";
251
757
  async function existsAsync(aPath) {
252
758
  return fs.promises.stat(aPath).then(() => true).catch((e) => false);
253
759
  }
@@ -272,6 +778,10 @@ function sha1File(filePath) {
272
778
  });
273
779
  });
274
780
  }
781
+ var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
782
+ function errorText(error) {
783
+ return FLAKINESS_DBG ? error.stack : error.message;
784
+ }
275
785
  function sha1Buffer(data) {
276
786
  const hash = crypto.createHash("sha1");
277
787
  hash.update(data);
@@ -283,9 +793,9 @@ async function retryWithBackoff(job, backoff = []) {
283
793
  return await job();
284
794
  } catch (e) {
285
795
  if (e instanceof AggregateError)
286
- console.error(`[flakiness.io err]`, e.errors[0].message);
796
+ console.error(`[flakiness.io err]`, errorText(e.errors[0]));
287
797
  else if (e instanceof Error)
288
- console.error(`[flakiness.io err]`, e.message);
798
+ console.error(`[flakiness.io err]`, errorText(e));
289
799
  else
290
800
  console.error(`[flakiness.io err]`, e);
291
801
  await new Promise((x) => setTimeout(x, timeout));
@@ -303,6 +813,7 @@ var httpUtils;
303
813
  reject = b;
304
814
  });
305
815
  const protocol = url.startsWith("https") ? https : http;
816
+ headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
306
817
  const request = protocol.request(url, { method, headers }, (res) => {
307
818
  const chunks = [];
308
819
  res.on("data", (chunk) => chunks.push(chunk));
@@ -362,13 +873,11 @@ function shell(command, args, options) {
362
873
  try {
363
874
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
364
875
  if (result.status !== 0) {
365
- console.log(result);
366
- console.log(options);
367
876
  return void 0;
368
877
  }
369
878
  return result.stdout.trim();
370
879
  } catch (e) {
371
- console.log(e);
880
+ console.error(e);
372
881
  return void 0;
373
882
  }
374
883
  }
@@ -424,6 +933,40 @@ function gitCommitInfo(gitRepo) {
424
933
  assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
425
934
  return sha.trim();
426
935
  }
936
+ async function resolveAttachmentPaths(report, attachmentsDir) {
937
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
938
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
939
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
940
+ const missingAttachments = /* @__PURE__ */ new Set();
941
+ FlakinessReport.visitTests(report, (test) => {
942
+ for (const attempt of test.attempts) {
943
+ for (const attachment of attempt.attachments ?? []) {
944
+ const attachmentPath = filenameToPath.get(attachment.id);
945
+ if (!attachmentPath) {
946
+ missingAttachments.add(attachment.id);
947
+ } else {
948
+ attachmentIdToPath.set(attachment.id, {
949
+ contentType: attachment.contentType,
950
+ id: attachment.id,
951
+ path: attachmentPath
952
+ });
953
+ }
954
+ }
955
+ }
956
+ });
957
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
958
+ }
959
+ async function listFilesRecursively(dir, result = []) {
960
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
961
+ for (const entry of entries) {
962
+ const fullPath = path.join(dir, entry.name);
963
+ if (entry.isDirectory())
964
+ await listFilesRecursively(fullPath, result);
965
+ else
966
+ result.push(fullPath);
967
+ }
968
+ return result;
969
+ }
427
970
  function computeGitRoot(somePathInsideGitRepo) {
428
971
  const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
429
972
  cwd: somePathInsideGitRepo,
@@ -484,7 +1027,75 @@ function createEnvironments(projects) {
484
1027
  return result;
485
1028
  }
486
1029
 
1030
+ // src/flakinessConfig.ts
1031
+ function createConfigPath(dir) {
1032
+ return path2.join(dir, ".flakiness", "config.json");
1033
+ }
1034
+ var gConfigPath;
1035
+ function ensureConfigPath() {
1036
+ if (!gConfigPath)
1037
+ gConfigPath = computeConfigPath();
1038
+ return gConfigPath;
1039
+ }
1040
+ function computeConfigPath() {
1041
+ for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
1042
+ const configPath = createConfigPath(p);
1043
+ if (fs2.existsSync(configPath))
1044
+ return configPath;
1045
+ }
1046
+ try {
1047
+ const gitRoot = computeGitRoot(process.cwd());
1048
+ return createConfigPath(gitRoot);
1049
+ } catch (e) {
1050
+ return createConfigPath(process.cwd());
1051
+ }
1052
+ }
1053
+ var FlakinessConfig = class _FlakinessConfig {
1054
+ constructor(_configPath, _config) {
1055
+ this._configPath = _configPath;
1056
+ this._config = _config;
1057
+ }
1058
+ static async load() {
1059
+ const configPath = ensureConfigPath();
1060
+ const data = await fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
1061
+ const json = data ? JSON.parse(data) : {};
1062
+ return new _FlakinessConfig(configPath, json);
1063
+ }
1064
+ static async projectOrDie(session2) {
1065
+ const config = await _FlakinessConfig.load();
1066
+ const projectPublicId = config.projectPublicId();
1067
+ if (!projectPublicId)
1068
+ throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
1069
+ const project = await session2.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
1070
+ if (!project)
1071
+ throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
1072
+ return project;
1073
+ }
1074
+ static createEmpty() {
1075
+ return new _FlakinessConfig(ensureConfigPath(), {});
1076
+ }
1077
+ path() {
1078
+ return this._configPath;
1079
+ }
1080
+ projectPublicId() {
1081
+ return this._config.projectPublicId;
1082
+ }
1083
+ setProjectPublicId(projectId) {
1084
+ this._config.projectPublicId = projectId;
1085
+ }
1086
+ async save() {
1087
+ await fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
1088
+ await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
1089
+ }
1090
+ };
1091
+
1092
+ // src/flakinessSession.ts
1093
+ import fs3 from "fs/promises";
1094
+ import os2 from "os";
1095
+ import path3 from "path";
1096
+
487
1097
  // src/serverapi.ts
1098
+ import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
488
1099
  function createServerAPI(endpoint, options) {
489
1100
  endpoint += "/api/";
490
1101
  const fetcher = options?.auth ? (url, init) => fetch(url, {
@@ -501,24 +1112,30 @@ function createServerAPI(endpoint, options) {
501
1112
 
502
1113
  // src/flakinessSession.ts
503
1114
  var CONFIG_DIR = (() => {
504
- const configDir = process.platform === "darwin" ? path.join(os2.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os2.homedir(), "AppData", "Roaming", "flakiness") : path.join(os2.homedir(), ".config", "flakiness");
1115
+ const configDir = process.platform === "darwin" ? path3.join(os2.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path3.join(os2.homedir(), "AppData", "Roaming", "flakiness") : path3.join(os2.homedir(), ".config", "flakiness");
505
1116
  return configDir;
506
1117
  })();
507
- var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
1118
+ var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
508
1119
  var FlakinessSession = class _FlakinessSession {
509
1120
  constructor(_config) {
510
1121
  this._config = _config;
511
1122
  this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
512
1123
  }
1124
+ static async loadOrDie() {
1125
+ const session2 = await _FlakinessSession.load();
1126
+ if (!session2)
1127
+ throw new Error(`Please login first with 'npx flakiness login'`);
1128
+ return session2;
1129
+ }
513
1130
  static async load() {
514
- const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
1131
+ const data = await fs3.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
515
1132
  if (!data)
516
1133
  return void 0;
517
1134
  const json = JSON.parse(data);
518
1135
  return new _FlakinessSession(json);
519
1136
  }
520
1137
  static async remove() {
521
- await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
1138
+ await fs3.unlink(CONFIG_PATH).catch((e) => void 0);
522
1139
  }
523
1140
  api;
524
1141
  endpoint() {
@@ -531,21 +1148,21 @@ var FlakinessSession = class _FlakinessSession {
531
1148
  return this._config.token;
532
1149
  }
533
1150
  async save() {
534
- await fs2.mkdir(CONFIG_DIR, { recursive: true });
535
- await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
1151
+ await fs3.mkdir(CONFIG_DIR, { recursive: true });
1152
+ await fs3.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
536
1153
  }
537
1154
  };
538
1155
 
539
1156
  // src/cli/cmd-convert.ts
540
- import fs4 from "fs/promises";
541
- import path3 from "path";
1157
+ import fs5 from "fs/promises";
1158
+ import path5 from "path";
542
1159
 
543
1160
  // src/junit.ts
544
1161
  import { FlakinessReport as FK } from "@flakiness/report";
545
1162
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
546
1163
  import assert2 from "assert";
547
- import fs3 from "fs";
548
- import path2 from "path";
1164
+ import fs4 from "fs";
1165
+ import path4 from "path";
549
1166
  function getProperties(element) {
550
1167
  const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
551
1168
  if (!propertiesNodes.length)
@@ -587,8 +1204,8 @@ function extractStdout(testcase, stdio) {
587
1204
  }));
588
1205
  }
589
1206
  async function parseAttachment(value) {
590
- let absolutePath = path2.resolve(process.cwd(), value);
591
- if (fs3.existsSync(absolutePath)) {
1207
+ let absolutePath = path4.resolve(process.cwd(), value);
1208
+ if (fs4.existsSync(absolutePath)) {
592
1209
  const id = await sha1File(absolutePath);
593
1210
  return {
594
1211
  contentType: "image/png",
@@ -660,7 +1277,7 @@ async function traverseJUnitReport(context, node) {
660
1277
  id: attachment.id,
661
1278
  contentType: attachment.contentType,
662
1279
  //TODO: better default names for attachments?
663
- name: attachment.path ? path2.basename(attachment.path) : `attachment`
1280
+ name: attachment.path ? path4.basename(attachment.path) : `attachment`
664
1281
  });
665
1282
  } else {
666
1283
  annotations.push({
@@ -735,15 +1352,15 @@ async function parseJUnit(xmls, options) {
735
1352
 
736
1353
  // src/cli/cmd-convert.ts
737
1354
  async function cmdConvert(junitPath, options) {
738
- const fullPath = path3.resolve(junitPath);
739
- if (!await fs4.access(fullPath, fs4.constants.F_OK).then(() => true).catch(() => false)) {
1355
+ const fullPath = path5.resolve(junitPath);
1356
+ if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
740
1357
  console.error(`Error: path ${fullPath} is not accessible`);
741
1358
  process.exit(1);
742
1359
  }
743
- const stat = await fs4.stat(fullPath);
1360
+ const stat = await fs5.stat(fullPath);
744
1361
  let xmlContents = [];
745
1362
  if (stat.isFile()) {
746
- const xmlContent = await fs4.readFile(fullPath, "utf-8");
1363
+ const xmlContent = await fs5.readFile(fullPath, "utf-8");
747
1364
  xmlContents.push(xmlContent);
748
1365
  } else if (stat.isDirectory()) {
749
1366
  const xmlFiles = await findXmlFiles(fullPath);
@@ -753,7 +1370,7 @@ async function cmdConvert(junitPath, options) {
753
1370
  }
754
1371
  console.log(`Found ${xmlFiles.length} XML files`);
755
1372
  for (const xmlFile of xmlFiles) {
756
- const xmlContent = await fs4.readFile(xmlFile, "utf-8");
1373
+ const xmlContent = await fs5.readFile(xmlFile, "utf-8");
757
1374
  xmlContents.push(xmlContent);
758
1375
  }
759
1376
  } else {
@@ -777,26 +1394,26 @@ async function cmdConvert(junitPath, options) {
777
1394
  runStartTimestamp: Date.now(),
778
1395
  runDuration: 0
779
1396
  });
780
- await fs4.writeFile("fkreport.json", JSON.stringify(report, null, 2));
1397
+ await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
781
1398
  console.log("\u2713 Saved report to fkreport.json");
782
1399
  if (attachments.length > 0) {
783
- await fs4.mkdir("fkattachments", { recursive: true });
1400
+ await fs5.mkdir("fkattachments", { recursive: true });
784
1401
  for (const attachment of attachments) {
785
1402
  if (attachment.path) {
786
- const destPath = path3.join("fkattachments", attachment.id);
787
- await fs4.copyFile(attachment.path, destPath);
1403
+ const destPath = path5.join("fkattachments", attachment.id);
1404
+ await fs5.copyFile(attachment.path, destPath);
788
1405
  } else if (attachment.body) {
789
- const destPath = path3.join("fkattachments", attachment.id);
790
- await fs4.writeFile(destPath, attachment.body);
1406
+ const destPath = path5.join("fkattachments", attachment.id);
1407
+ await fs5.writeFile(destPath, attachment.body);
791
1408
  }
792
1409
  }
793
1410
  console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
794
1411
  }
795
1412
  }
796
1413
  async function findXmlFiles(dir, result = []) {
797
- const entries = await fs4.readdir(dir, { withFileTypes: true });
1414
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
798
1415
  for (const entry of entries) {
799
- const fullPath = path3.join(dir, entry.name);
1416
+ const fullPath = path5.join(dir, entry.name);
800
1417
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
801
1418
  result.push(fullPath);
802
1419
  else if (entry.isDirectory())
@@ -807,53 +1424,8 @@ async function findXmlFiles(dir, result = []) {
807
1424
 
808
1425
  // src/cli/cmd-download.ts
809
1426
  import fs6 from "fs";
810
- import path5 from "path";
811
-
812
- // src/flakinessLink.ts
813
- import fs5 from "fs/promises";
814
- import path4 from "path";
815
- var GIT_ROOT = computeGitRoot(process.cwd());
816
- var CONFIG_DIR2 = path4.join(GIT_ROOT, ".flakiness");
817
- var CONFIG_PATH2 = path4.join(CONFIG_DIR2, "config.json");
818
- var FlakinessLink = class _FlakinessLink {
819
- constructor(_config) {
820
- this._config = _config;
821
- }
822
- static async load() {
823
- const data = await fs5.readFile(CONFIG_PATH2, "utf-8").catch((e) => void 0);
824
- if (!data)
825
- return void 0;
826
- const json = JSON.parse(data);
827
- return new _FlakinessLink(json);
828
- }
829
- static async remove() {
830
- await fs5.unlink(CONFIG_PATH2).catch((e) => void 0);
831
- }
832
- path() {
833
- return CONFIG_PATH2;
834
- }
835
- projectId() {
836
- return this._config.projectId;
837
- }
838
- async save() {
839
- await fs5.mkdir(CONFIG_DIR2, { recursive: true });
840
- await fs5.writeFile(CONFIG_PATH2, JSON.stringify(this._config, null, 2));
841
- }
842
- };
843
-
844
- // src/cli/cmd-download.ts
845
- async function cmdDownload(runId) {
846
- const session2 = await FlakinessSession.load();
847
- if (!session2) {
848
- console.log(`Please login first`);
849
- process.exit(1);
850
- }
851
- const link = await FlakinessLink.load();
852
- if (!link) {
853
- console.log(`Please run 'npx flakiness link' to link to the project`);
854
- process.exit(1);
855
- }
856
- const project = await session2.api.project.getProject.GET({ projectPublicId: link.projectId() });
1427
+ import path6 from "path";
1428
+ async function cmdDownload(session2, project, runId) {
857
1429
  const urls = await session2.api.run.downloadURLs.GET({
858
1430
  orgSlug: project.org.orgSlug,
859
1431
  projectSlug: project.projectSlug,
@@ -864,7 +1436,7 @@ async function cmdDownload(runId) {
864
1436
  console.log(`Directory ${rootDir} already exists!`);
865
1437
  return;
866
1438
  }
867
- const attachmentsDir = path5.join(rootDir, "attachments");
1439
+ const attachmentsDir = path6.join(rootDir, "attachments");
868
1440
  await fs6.promises.mkdir(rootDir, { recursive: true });
869
1441
  if (urls.attachmentURLs.length)
870
1442
  await fs6.promises.mkdir(attachmentsDir, { recursive: true });
@@ -872,7 +1444,7 @@ async function cmdDownload(runId) {
872
1444
  if (!response.ok)
873
1445
  throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
874
1446
  const reportContent = await response.text();
875
- await fs6.promises.writeFile(path5.join(rootDir, "report.json"), reportContent);
1447
+ await fs6.promises.writeFile(path6.join(rootDir, "report.json"), reportContent);
876
1448
  const attachmentDownloader = async () => {
877
1449
  while (urls.attachmentURLs.length) {
878
1450
  const url = urls.attachmentURLs.pop();
@@ -880,8 +1452,8 @@ async function cmdDownload(runId) {
880
1452
  if (!response2.ok)
881
1453
  throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
882
1454
  const fileBuffer = Buffer.from(await response2.arrayBuffer());
883
- const filename = path5.basename(new URL(url).pathname);
884
- await fs6.promises.writeFile(path5.join(attachmentsDir, filename), fileBuffer);
1455
+ const filename = path6.basename(new URL(url).pathname);
1456
+ await fs6.promises.writeFile(path6.join(attachmentsDir, filename), fileBuffer);
885
1457
  }
886
1458
  };
887
1459
  const workerPromises = [];
@@ -907,11 +1479,10 @@ async function cmdLink(slug) {
907
1479
  console.log(`Failed to find project ${slug}`);
908
1480
  process.exit(1);
909
1481
  }
910
- const link = new FlakinessLink({
911
- projectId: project.projectPublicId
912
- });
913
- await link.save();
914
- console.log(`\u2713 Link successful! Config saved to ${link.path()}`);
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}`);
915
1486
  }
916
1487
 
917
1488
  // ../server/lib/common/knownClientIds.js
@@ -921,6 +1492,7 @@ var KNOWN_CLIENT_IDS = {
921
1492
  };
922
1493
 
923
1494
  // src/cli/cmd-login.ts
1495
+ import open from "open";
924
1496
  import os3 from "os";
925
1497
  async function cmdLogin(endpoint) {
926
1498
  const api = createServerAPI(endpoint);
@@ -928,6 +1500,7 @@ async function cmdLogin(endpoint) {
928
1500
  clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
929
1501
  name: os3.hostname()
930
1502
  });
1503
+ await open(new URL(data.verificationUrl, endpoint).href);
931
1504
  console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
932
1505
  let token;
933
1506
  while (Date.now() < data.deadline) {
@@ -949,15 +1522,226 @@ async function cmdLogin(endpoint) {
949
1522
  endpoint,
950
1523
  token
951
1524
  });
952
- await session2.save();
953
- console.log(`\u2713 Login successful! Token saved to ${session2.path()}`);
1525
+ try {
1526
+ const user = await session2.api.user.whoami.GET();
1527
+ await session2.save();
1528
+ console.log(`\u2713 Logged in as ${user.userName} (${user.userLogin})`);
1529
+ } catch (e) {
1530
+ const message = e instanceof Error ? e.message : String(e);
1531
+ console.error(`x Failed to login:`, message);
1532
+ }
954
1533
  }
955
1534
 
956
1535
  // src/cli/cmd-logout.ts
957
1536
  async function cmdLogout() {
1537
+ const session2 = await FlakinessSession.load();
1538
+ if (!session2)
1539
+ return;
1540
+ const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
1541
+ if (currentSession)
1542
+ await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
958
1543
  await FlakinessSession.remove();
959
1544
  }
960
1545
 
1546
+ // src/cli/cmd-show-report.ts
1547
+ import chalk from "chalk";
1548
+ import open2 from "open";
1549
+ import path7 from "path";
1550
+
1551
+ // src/localReportServer.ts
1552
+ import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
1553
+ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
1554
+ import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
1555
+ import bodyParser from "body-parser";
1556
+ import compression from "compression";
1557
+ import debug from "debug";
1558
+ import express from "express";
1559
+ import "express-async-errors";
1560
+ import fs8 from "fs";
1561
+ import http2 from "http";
1562
+
1563
+ // src/localGit.ts
1564
+ import { exec } from "child_process";
1565
+ import { promisify } from "util";
1566
+ var execAsync = promisify(exec);
1567
+ async function listLocalCommits(gitRoot, head, count) {
1568
+ const FIELD_SEPARATOR = "|~|";
1569
+ const RECORD_SEPARATOR = "\0";
1570
+ const prettyFormat = [
1571
+ "%H",
1572
+ // %H: Full commit hash
1573
+ "%at",
1574
+ // %at: Author date as a Unix timestamp (seconds since epoch)
1575
+ "%an",
1576
+ // %an: Author name
1577
+ "%s"
1578
+ // %s: Subject (the first line of the commit message)
1579
+ ].join(FIELD_SEPARATOR);
1580
+ const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
1581
+ try {
1582
+ const { stdout } = await execAsync(command, { cwd: gitRoot });
1583
+ if (!stdout) {
1584
+ return [];
1585
+ }
1586
+ return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
1587
+ const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
1588
+ return {
1589
+ commitId,
1590
+ timestamp: parseInt(timestampStr, 10) * 1e3,
1591
+ // Convert timestamp from seconds to milliseconds
1592
+ author,
1593
+ message,
1594
+ walkIndex: 0
1595
+ };
1596
+ });
1597
+ } catch (error) {
1598
+ console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
1599
+ throw error;
1600
+ }
1601
+ }
1602
+
1603
+ // src/localReportApi.ts
1604
+ import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
1605
+ import fs7 from "fs";
1606
+ import { z } from "zod/v4";
1607
+ var t = TypedHTTP2.Router.create();
1608
+ var localReportRouter = {
1609
+ ping: t.get({
1610
+ handler: async () => {
1611
+ return "pong";
1612
+ }
1613
+ }),
1614
+ lastCommits: t.get({
1615
+ handler: async ({ ctx }) => {
1616
+ return ctx.commits;
1617
+ }
1618
+ }),
1619
+ report: {
1620
+ attachment: t.rawMethod("GET", {
1621
+ input: z.object({
1622
+ attachmentId: z.string().min(1).max(100).transform((id) => id)
1623
+ }),
1624
+ handler: async ({ ctx, input }) => {
1625
+ const idx = ctx.attachmentIdToPath.get(input.attachmentId);
1626
+ if (!idx)
1627
+ throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
1628
+ const buffer = await fs7.promises.readFile(idx.path);
1629
+ return TypedHTTP2.ok(buffer, idx.contentType);
1630
+ }
1631
+ }),
1632
+ json: t.get({
1633
+ handler: async ({ ctx }) => {
1634
+ return ctx.report;
1635
+ }
1636
+ })
1637
+ }
1638
+ };
1639
+
1640
+ // src/localReportServer.ts
1641
+ var logHTTPServer = debug("fk:http");
1642
+ var LocalReportServer = class _LocalReportServer {
1643
+ constructor(_server, _port, _authToken) {
1644
+ this._server = _server;
1645
+ this._port = _port;
1646
+ this._authToken = _authToken;
1647
+ }
1648
+ static async create(options) {
1649
+ const app = express();
1650
+ app.set("etag", false);
1651
+ const authToken = randomUUIDBase62();
1652
+ app.use(compression());
1653
+ app.use(bodyParser.json({ limit: 256 * 1024 }));
1654
+ app.use((req, res, next) => {
1655
+ if (!req.path.startsWith("/" + authToken))
1656
+ throw TypedHTTP3.HttpError.withCode("UNAUTHORIZED");
1657
+ res.setHeader("Access-Control-Allow-Headers", "*");
1658
+ res.setHeader("Access-Control-Allow-Origin", options.endpoint);
1659
+ res.setHeader("Access-Control-Allow-Methods", "*");
1660
+ if (req.method === "OPTIONS") {
1661
+ res.writeHead(204);
1662
+ res.end();
1663
+ return;
1664
+ }
1665
+ req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
1666
+ res.on("close", () => {
1667
+ if (!res.headersSent)
1668
+ logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
1669
+ });
1670
+ next();
1671
+ });
1672
+ app.use("/" + authToken, createTypedHttpExpressMiddleware({
1673
+ router: localReportRouter,
1674
+ createRootContext: async ({ req, res, input }) => {
1675
+ const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
1676
+ const attachmentsDir = options.attachmentsFolder;
1677
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1678
+ if (missingAttachments.length) {
1679
+ const first = missingAttachments.slice(0, 3);
1680
+ for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
1681
+ console.warn(`Missing attachment with id ${missingAttachments[i]}`);
1682
+ if (missingAttachments.length > 3)
1683
+ console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
1684
+ }
1685
+ return {
1686
+ report,
1687
+ commits: await listLocalCommits(process.cwd(), report.commitId, 100),
1688
+ attachmentIdToPath
1689
+ };
1690
+ }
1691
+ }));
1692
+ app.use((err, req, res, next) => {
1693
+ if (err instanceof TypedHTTP3.HttpError)
1694
+ return res.status(err.status).send({ error: err.message });
1695
+ logHTTPServer(err);
1696
+ res.status(500).send({ error: "Internal Server Error" });
1697
+ });
1698
+ const server = http2.createServer(app);
1699
+ server.on("error", (err) => {
1700
+ if (err.code === "ECONNRESET") {
1701
+ logHTTPServer("Client connection reset. Ignoring.");
1702
+ return;
1703
+ }
1704
+ throw err;
1705
+ });
1706
+ const port = await new Promise((resolve) => server.listen(options.port, () => {
1707
+ resolve(server.address().port);
1708
+ }));
1709
+ return new _LocalReportServer(server, port, authToken);
1710
+ }
1711
+ authToken() {
1712
+ return this._authToken;
1713
+ }
1714
+ port() {
1715
+ return this._port;
1716
+ }
1717
+ async dispose() {
1718
+ await new Promise((x) => this._server.close(x));
1719
+ }
1720
+ };
1721
+
1722
+ // src/cli/cmd-show-report.ts
1723
+ async function cmdShowReport(reportFolder) {
1724
+ const reportPath = path7.join(reportFolder, "report.json");
1725
+ const session2 = await FlakinessSession.load();
1726
+ const config = await FlakinessConfig.load();
1727
+ const projectPublicId = config.projectPublicId();
1728
+ const project = projectPublicId && session2 ? await session2.api.project.getProject.GET({ projectPublicId }) : void 0;
1729
+ const endpoint = session2?.endpoint() ?? "https://flakiness.io";
1730
+ const server = await LocalReportServer.create({
1731
+ endpoint,
1732
+ port: 9373,
1733
+ reportPath,
1734
+ attachmentsFolder: reportFolder
1735
+ });
1736
+ const reportEndpoint = project ? `${endpoint}/localreport/${project.org.orgSlug}/${project.projectSlug}?port=${server.port()}&token=${server.authToken()}` : `${endpoint}/localreport?port=${server.port()}&token=${server.authToken()}`;
1737
+ console.log(chalk.cyan(`
1738
+ Serving Flakiness report at ${reportEndpoint}
1739
+ Press Ctrl+C to quit.`));
1740
+ await open2(reportEndpoint);
1741
+ await new Promise(() => {
1742
+ });
1743
+ }
1744
+
961
1745
  // src/cli/cmd-status.ts
962
1746
  async function cmdStatus() {
963
1747
  const session2 = await FlakinessSession.load();
@@ -967,31 +1751,34 @@ async function cmdStatus() {
967
1751
  }
968
1752
  const user = await session2.api.user.whoami.GET();
969
1753
  console.log(`user: ${user.userName} (${user.userLogin})`);
970
- const link = await FlakinessLink.load();
971
- if (!link) {
1754
+ const config = await FlakinessConfig.load();
1755
+ const projectPublicId = config.projectPublicId();
1756
+ if (!projectPublicId) {
972
1757
  console.log(`project: <not linked>`);
973
1758
  return;
974
1759
  }
975
- const project = await session2.api.project.getProject.GET({
976
- projectPublicId: link.projectId()
977
- });
1760
+ const project = await session2.api.project.getProject.GET({ projectPublicId });
978
1761
  console.log(`project: ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
979
1762
  }
980
1763
 
981
1764
  // src/cli/cmd-unlink.ts
982
1765
  async function cmdUnlink() {
983
- await FlakinessLink.remove();
1766
+ const config = await FlakinessConfig.load();
1767
+ if (!config.projectPublicId())
1768
+ return;
1769
+ config.setProjectPublicId(void 0);
1770
+ await config.save();
984
1771
  }
985
1772
 
986
1773
  // src/cli/cmd-upload-playwright-json.ts
987
- import fs8 from "fs/promises";
988
- import path6 from "path";
1774
+ import fs10 from "fs/promises";
1775
+ import path8 from "path";
989
1776
 
990
1777
  // src/playwrightJSONReport.ts
991
- import { FlakinessReport as FK2, FlakinessReport } from "@flakiness/report";
992
- import debug from "debug";
1778
+ import { FlakinessReport as FK2, FlakinessReport as FlakinessReport2 } from "@flakiness/report";
1779
+ import debug2 from "debug";
993
1780
  import { posix as posixPath2 } from "path";
994
- var dlog = debug("flakiness:json-report");
1781
+ var dlog = debug2("flakiness:json-report");
995
1782
  var PlaywrightJSONReport;
996
1783
  ((PlaywrightJSONReport2) => {
997
1784
  function collectMetadata(somePathInsideProject = process.cwd()) {
@@ -1023,7 +1810,7 @@ var PlaywrightJSONReport;
1023
1810
  };
1024
1811
  const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
1025
1812
  const report = {
1026
- category: FlakinessReport.CATEGORY_PLAYWRIGHT,
1813
+ category: FlakinessReport2.CATEGORY_PLAYWRIGHT,
1027
1814
  commitId: metadata.commitId,
1028
1815
  configPath,
1029
1816
  url: metadata.runURL,
@@ -1163,9 +1950,10 @@ function parseJSONError(context, error) {
1163
1950
  }
1164
1951
 
1165
1952
  // src/reportUploader.ts
1166
- import fs7 from "fs";
1953
+ import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
1954
+ import assert3 from "assert";
1955
+ import fs9 from "fs";
1167
1956
  import { URL as URL2 } from "url";
1168
- import { brotliCompressSync as brotliCompressSync2 } from "zlib";
1169
1957
  var ReportUploader = class _ReportUploader {
1170
1958
  static optionsFromEnv(overrides) {
1171
1959
  const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
@@ -1177,7 +1965,8 @@ var ReportUploader = class _ReportUploader {
1177
1965
  static async upload(options) {
1178
1966
  const uploaderOptions = _ReportUploader.optionsFromEnv(options);
1179
1967
  if (!uploaderOptions) {
1180
- options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
1968
+ if (process.env.CI)
1969
+ options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
1181
1970
  return void 0;
1182
1971
  }
1183
1972
  const uploader = new _ReportUploader(uploaderOptions);
@@ -1210,11 +1999,10 @@ var ReportUpload = class {
1210
1999
  this._options = options;
1211
2000
  this._report = report;
1212
2001
  this._attachments = attachments;
1213
- this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
2002
+ this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
1214
2003
  }
1215
2004
  async upload(options) {
1216
2005
  const response = await this._api.run.startUpload.POST({
1217
- flakinessAccessToken: this._options.flakinessAccessToken,
1218
2006
  attachmentIds: this._attachments.map((attachment) => attachment.id)
1219
2007
  }).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
1220
2008
  if (response?.error || !response.result)
@@ -1225,7 +2013,7 @@ var ReportUpload = class {
1225
2013
  const uploadURL = response.result.attachment_upload_urls[attachment.id];
1226
2014
  if (!uploadURL)
1227
2015
  throw new Error("Internal error: missing upload URL for attachment!");
1228
- return this._uploadAttachment(attachment, uploadURL);
2016
+ return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
1229
2017
  })
1230
2018
  ]);
1231
2019
  const response2 = await this._api.run.completeUpload.POST({
@@ -1235,7 +2023,7 @@ var ReportUpload = class {
1235
2023
  return { success: true, reportUrl: url };
1236
2024
  }
1237
2025
  async _uploadReport(data, uploadUrl, syncCompression) {
1238
- const compressed = syncCompression ? brotliCompressSync2(data) : await brotliCompressAsync(data);
2026
+ const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
1239
2027
  const headers = {
1240
2028
  "Content-Type": "application/json",
1241
2029
  "Content-Length": Buffer.byteLength(compressed) + "",
@@ -1252,11 +2040,34 @@ var ReportUpload = class {
1252
2040
  await responseDataPromise;
1253
2041
  }, HTTP_BACKOFF);
1254
2042
  }
1255
- async _uploadAttachment(attachment, uploadUrl) {
1256
- const bytesLength = attachment.path ? (await fs7.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
2043
+ async _uploadAttachment(attachment, uploadUrl, syncCompression) {
2044
+ const mimeType = attachment.contentType.toLocaleLowerCase().trim();
2045
+ const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
2046
+ if (!compressable && attachment.path) {
2047
+ const attachmentPath = attachment.path;
2048
+ await retryWithBackoff(async () => {
2049
+ const { request, responseDataPromise } = httpUtils.createRequest({
2050
+ url: uploadUrl,
2051
+ headers: {
2052
+ "Content-Type": attachment.contentType,
2053
+ "Content-Length": (await fs9.promises.stat(attachmentPath)).size + ""
2054
+ },
2055
+ method: "put"
2056
+ });
2057
+ fs9.createReadStream(attachmentPath).pipe(request);
2058
+ await responseDataPromise;
2059
+ }, HTTP_BACKOFF);
2060
+ return;
2061
+ }
2062
+ let buffer = attachment.body ? attachment.body : attachment.path ? await fs9.promises.readFile(attachment.path) : void 0;
2063
+ assert3(buffer);
2064
+ const encoding = compressable ? "br" : void 0;
2065
+ if (compressable)
2066
+ buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
1257
2067
  const headers = {
1258
2068
  "Content-Type": attachment.contentType,
1259
- "Content-Length": bytesLength + ""
2069
+ "Content-Length": Buffer.byteLength(buffer) + "",
2070
+ "Content-Encoding": encoding
1260
2071
  };
1261
2072
  await retryWithBackoff(async () => {
1262
2073
  const { request, responseDataPromise } = httpUtils.createRequest({
@@ -1264,13 +2075,8 @@ var ReportUpload = class {
1264
2075
  headers,
1265
2076
  method: "put"
1266
2077
  });
1267
- if (attachment.path) {
1268
- fs7.createReadStream(attachment.path).pipe(request);
1269
- } else {
1270
- if (attachment.body)
1271
- request.write(attachment.body);
1272
- request.end();
1273
- }
2078
+ request.write(buffer);
2079
+ request.end();
1274
2080
  await responseDataPromise;
1275
2081
  }, HTTP_BACKOFF);
1276
2082
  }
@@ -1278,12 +2084,12 @@ var ReportUpload = class {
1278
2084
 
1279
2085
  // src/cli/cmd-upload-playwright-json.ts
1280
2086
  async function cmdUploadPlaywrightJson(relativePath, options) {
1281
- const fullPath = path6.resolve(relativePath);
1282
- if (!await fs8.access(fullPath, fs8.constants.F_OK).then(() => true).catch(() => false)) {
2087
+ const fullPath = path8.resolve(relativePath);
2088
+ if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
1283
2089
  console.error(`Error: path ${fullPath} is not accessible`);
1284
2090
  process.exit(1);
1285
2091
  }
1286
- const text = await fs8.readFile(fullPath, "utf-8");
2092
+ const text = await fs10.readFile(fullPath, "utf-8");
1287
2093
  const playwrightJson = JSON.parse(text);
1288
2094
  const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
1289
2095
  extractAttachments: true
@@ -1304,38 +2110,18 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
1304
2110
  }
1305
2111
 
1306
2112
  // src/cli/cmd-upload.ts
1307
- import { FlakinessReport as FlakinessReport2 } from "@flakiness/report";
1308
- import fs9 from "fs/promises";
1309
- import path7 from "path";
2113
+ import fs11 from "fs/promises";
2114
+ import path9 from "path";
1310
2115
  async function cmdUpload(relativePath, options) {
1311
- const fullPath = path7.resolve(relativePath);
1312
- if (!await fs9.access(fullPath, fs9.constants.F_OK).then(() => true).catch(() => false)) {
2116
+ const fullPath = path9.resolve(relativePath);
2117
+ if (!await fs11.access(fullPath, fs11.constants.F_OK).then(() => true).catch(() => false)) {
1313
2118
  console.error(`Error: path ${fullPath} is not accessible`);
1314
2119
  process.exit(1);
1315
2120
  }
1316
- const attachmentsDir = options.attachmentsDir ?? path7.dirname(fullPath);
1317
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
1318
- const attachmentIdToPath = new Map(attachmentFiles.map((file) => [path7.basename(file), file]));
1319
- const text = await fs9.readFile(fullPath, "utf-8");
2121
+ const text = await fs11.readFile(fullPath, "utf-8");
1320
2122
  const report = JSON.parse(text);
1321
- const attachments = [];
1322
- const missingAttachments = [];
1323
- FlakinessReport2.visitTests(report, (test) => {
1324
- for (const attempt of test.attempts) {
1325
- for (const attachment of attempt.attachments ?? []) {
1326
- const file = attachmentIdToPath.get(attachment.id);
1327
- if (!file) {
1328
- missingAttachments.push(attachment);
1329
- continue;
1330
- }
1331
- attachments.push({
1332
- contentType: attachment.contentType,
1333
- id: attachment.id,
1334
- path: file
1335
- });
1336
- }
1337
- }
1338
- });
2123
+ const attachmentsDir = options.attachmentsDir ?? path9.dirname(fullPath);
2124
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1339
2125
  if (missingAttachments.length && !options.ignoreMissingAttachments) {
1340
2126
  console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
1341
2127
  process.exit(1);
@@ -1344,7 +2130,7 @@ async function cmdUpload(relativePath, options) {
1344
2130
  flakinessAccessToken: options.accessToken,
1345
2131
  flakinessEndpoint: options.endpoint
1346
2132
  });
1347
- const upload = uploader.createUpload(report, attachments);
2133
+ const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
1348
2134
  const uploadResult = await upload.upload();
1349
2135
  if (!uploadResult.success) {
1350
2136
  console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
@@ -1352,17 +2138,6 @@ async function cmdUpload(relativePath, options) {
1352
2138
  console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
1353
2139
  }
1354
2140
  }
1355
- async function listFilesRecursively(dir, result = []) {
1356
- const entries = await fs9.readdir(dir, { withFileTypes: true });
1357
- for (const entry of entries) {
1358
- const fullPath = path7.join(dir, entry.name);
1359
- if (entry.isDirectory())
1360
- await listFilesRecursively(fullPath, result);
1361
- else
1362
- result.push(fullPath);
1363
- }
1364
- return result;
1365
- }
1366
2141
 
1367
2142
  // src/cli/cmd-whoami.ts
1368
2143
  async function cmdWhoami() {
@@ -1378,7 +2153,7 @@ async function cmdWhoami() {
1378
2153
 
1379
2154
  // src/cli/cli.ts
1380
2155
  var session = await FlakinessSession.load();
1381
- var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").makeOptionMandatory().env("FLAKINESS_ACCESS_TOKEN");
2156
+ var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
1382
2157
  var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
1383
2158
  var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
1384
2159
  var optIgnoreMissingAttachments = new Option("--ignore-missing-attachments", "Upload report even if some attachments are missing.").default("", "Same directory as the report file");
@@ -1388,20 +2163,42 @@ async function runCommand(callback) {
1388
2163
  } catch (e) {
1389
2164
  if (!(e instanceof Error))
1390
2165
  throw e;
1391
- if (process.env.DBG)
1392
- console.error(e.stack);
1393
- else
1394
- console.error(e.message);
2166
+ console.error(errorText(e));
1395
2167
  process.exit(1);
1396
2168
  }
1397
2169
  }
1398
- var PACKAGE_JSON = JSON.parse(fs10.readFileSync(path8.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
2170
+ var PACKAGE_JSON = JSON.parse(fs12.readFileSync(path10.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
1399
2171
  var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
2172
+ async function ensureAccessToken(options) {
2173
+ let accessToken = options.accessToken;
2174
+ if (!accessToken) {
2175
+ const config = await FlakinessConfig.load();
2176
+ const projectPublicId = config.projectPublicId();
2177
+ if (session && projectPublicId) {
2178
+ try {
2179
+ accessToken = (await session.api.project.getProject.GET({ projectPublicId })).readWriteAccessToken;
2180
+ } catch (e) {
2181
+ if (e instanceof TypedHTTP4.HttpError && e.status === 404) {
2182
+ } else {
2183
+ throw e;
2184
+ }
2185
+ }
2186
+ }
2187
+ }
2188
+ assert4(accessToken, `Please either pass FLAKINESS_ACCESS_TOKEN or run login + link`);
2189
+ return {
2190
+ ...options,
2191
+ accessToken
2192
+ };
2193
+ }
1400
2194
  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 () => {
1401
- await cmdUploadPlaywrightJson(relativePath, options);
2195
+ await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
1402
2196
  }));
1403
- program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
2197
+ 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 () => {
1404
2199
  await cmdLogin(options.endpoint);
2200
+ if (options.link)
2201
+ await cmdLink(options.link);
1405
2202
  }));
1406
2203
  program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
1407
2204
  await cmdLogout();
@@ -1418,14 +2215,60 @@ program.command("unlink").description("Unlink repository from the flakiness proj
1418
2215
  program.command("status").description("Status repository from the flakiness project").action(async () => runCommand(async () => {
1419
2216
  await cmdStatus();
1420
2217
  }));
1421
- program.command("download").description("Download run").argument("runId", "Run id to download").action(async (runId) => runCommand(async () => {
1422
- await cmdDownload(parseInt(runId, 10));
2218
+ var optRunId = new Option("--run-id <runId>", "RunId flakiness.io access token").argParser((value) => {
2219
+ const parsed = parseInt(value, 10);
2220
+ if (isNaN(parsed) || parsed < 1) {
2221
+ throw new Error("runId must be a number >= 1");
2222
+ }
2223
+ return parsed;
2224
+ });
2225
+ var optSince = new Option("--since <date>", "Start date for filtering").argParser((value) => {
2226
+ const parsed = new Date(value);
2227
+ if (isNaN(parsed.getTime())) {
2228
+ throw new Error("Invalid date format");
2229
+ }
2230
+ return parsed;
2231
+ });
2232
+ var optParallel = new Option("-j, --parallel <date>", "Parallel jobs to run").argParser((value) => {
2233
+ const parsed = parseInt(value, 10);
2234
+ if (isNaN(parsed) || parsed < 1) {
2235
+ throw new Error("parallel must be a number >= 1");
2236
+ }
2237
+ return parsed;
2238
+ });
2239
+ program.command("download").description("Download run").addOption(optSince).addOption(optRunId).addOption(optParallel).action(async (options) => runCommand(async () => {
2240
+ const session2 = await FlakinessSession.loadOrDie();
2241
+ const project = await FlakinessConfig.projectOrDie(session2);
2242
+ let runIds = [];
2243
+ if (options.runId) {
2244
+ runIds = [options.runId, options.runId];
2245
+ } else if (options.since) {
2246
+ runIds = await session2.api.project.listRuns.GET({
2247
+ orgSlug: project.org.orgSlug,
2248
+ projectSlug: project.projectSlug,
2249
+ sinceTimestampMs: +options.since
2250
+ });
2251
+ console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
2252
+ }
2253
+ const it = Ranges.iterate(runIds);
2254
+ const downloaders = [];
2255
+ for (let i = 0; i < (options.parallel ?? 1); ++i) {
2256
+ downloaders.push((async () => {
2257
+ for (let result = it.next(); !result.done; result = it.next())
2258
+ await cmdDownload(session2, project, result.value);
2259
+ })());
2260
+ }
2261
+ await Promise.all(downloaders);
1423
2262
  }));
1424
2263
  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).addOption(optIgnoreMissingAttachments).action(async (relativePath, options) => {
1425
2264
  await runCommand(async () => {
1426
- await cmdUpload(relativePath, options);
2265
+ await cmdUpload(relativePath, await ensureAccessToken(options));
1427
2266
  });
1428
2267
  });
2268
+ 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 () => {
2269
+ const dir = path10.join(process.cwd(), arg ?? "flakiness-report");
2270
+ await cmdShowReport(dir);
2271
+ }));
1429
2272
  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) => {
1430
2273
  await runCommand(async () => {
1431
2274
  await cmdConvert(junitPath, options);