@flakiness/sdk 0.95.0 → 0.96.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);
40
+ }
41
+ let result = "";
42
+ for (let i = 0; i < value.length; i += 1) {
43
+ result += encode_integer(value[i]);
104
44
  }
105
- TypedHTTP2.isInformationalResponse = isInformationalResponse;
106
- function isSuccessResponse(response) {
107
- return response.status >= 200 && response.status < 300;
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;
53
+ }
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;
108
113
  }
109
- TypedHTTP2.isSuccessResponse = isSuccessResponse;
110
- function isRedirectResponse(response) {
111
- return response.status >= 300 && response.status < 400;
114
+ push(element, score) {
115
+ this._heap.push({ element, score });
116
+ this._up(this._heap.length - 1);
112
117
  }
113
- TypedHTTP2.isRedirectResponse = isRedirectResponse;
114
- function isErrorResponse(response) {
115
- return response.status >= 400 && response.status < 600;
118
+ get size() {
119
+ return this._heap.length;
116
120
  }
117
- TypedHTTP2.isErrorResponse = isErrorResponse;
118
- function info(status) {
119
- return { status };
121
+ peekEntry() {
122
+ return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
120
123
  }
121
- TypedHTTP2.info = info;
122
- function ok(data, status) {
123
- return {
124
- status: status ?? TypedHTTP2.StatusCodes.Success.OK,
125
- data
126
- };
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];
127
134
  }
128
- TypedHTTP2.ok = ok;
129
- function redirect(url, status = 302) {
130
- return { status, url };
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);
163
+ }
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;
255
+ }
256
+ return lo;
131
257
  }
132
- TypedHTTP2.redirect = redirect;
133
- function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
134
- return { status, message };
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
+ }
135
369
  }
136
- TypedHTTP2.error = error;
137
- class Router {
138
- constructor(_resolveContext) {
139
- this._resolveContext = _resolveContext;
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);
140
378
  }
141
- static create() {
142
- return new Router(async (e) => e.ctx);
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;
143
390
  }
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
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
+ }
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
+ }
172
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));
@@ -424,6 +935,40 @@ function gitCommitInfo(gitRepo) {
424
935
  assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
425
936
  return sha.trim();
426
937
  }
938
+ async function resolveAttachmentPaths(report, attachmentsDir) {
939
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
940
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
941
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
942
+ const missingAttachments = /* @__PURE__ */ new Set();
943
+ FlakinessReport.visitTests(report, (test) => {
944
+ for (const attempt of test.attempts) {
945
+ for (const attachment of attempt.attachments ?? []) {
946
+ const attachmentPath = filenameToPath.get(attachment.id);
947
+ if (!attachmentPath) {
948
+ missingAttachments.add(attachment.id);
949
+ } else {
950
+ attachmentIdToPath.set(attachment.id, {
951
+ contentType: attachment.contentType,
952
+ id: attachment.id,
953
+ path: attachmentPath
954
+ });
955
+ }
956
+ }
957
+ }
958
+ });
959
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
960
+ }
961
+ async function listFilesRecursively(dir, result = []) {
962
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
963
+ for (const entry of entries) {
964
+ const fullPath = path.join(dir, entry.name);
965
+ if (entry.isDirectory())
966
+ await listFilesRecursively(fullPath, result);
967
+ else
968
+ result.push(fullPath);
969
+ }
970
+ return result;
971
+ }
427
972
  function computeGitRoot(somePathInsideGitRepo) {
428
973
  const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
429
974
  cwd: somePathInsideGitRepo,
@@ -484,7 +1029,75 @@ function createEnvironments(projects) {
484
1029
  return result;
485
1030
  }
486
1031
 
1032
+ // src/flakinessConfig.ts
1033
+ function createConfigPath(dir) {
1034
+ return path2.join(dir, ".flakiness", "config.json");
1035
+ }
1036
+ var gConfigPath;
1037
+ function ensureConfigPath() {
1038
+ if (!gConfigPath)
1039
+ gConfigPath = computeConfigPath();
1040
+ return gConfigPath;
1041
+ }
1042
+ function computeConfigPath() {
1043
+ for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
1044
+ const configPath = createConfigPath(p);
1045
+ if (fs2.existsSync(configPath))
1046
+ return configPath;
1047
+ }
1048
+ try {
1049
+ const gitRoot = computeGitRoot(process.cwd());
1050
+ return createConfigPath(gitRoot);
1051
+ } catch (e) {
1052
+ return createConfigPath(process.cwd());
1053
+ }
1054
+ }
1055
+ var FlakinessConfig = class _FlakinessConfig {
1056
+ constructor(_configPath, _config) {
1057
+ this._configPath = _configPath;
1058
+ this._config = _config;
1059
+ }
1060
+ static async load() {
1061
+ const configPath = ensureConfigPath();
1062
+ const data = await fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
1063
+ const json = data ? JSON.parse(data) : {};
1064
+ return new _FlakinessConfig(configPath, json);
1065
+ }
1066
+ static async projectOrDie(session2) {
1067
+ const config = await _FlakinessConfig.load();
1068
+ const projectPublicId = config.projectPublicId();
1069
+ if (!projectPublicId)
1070
+ throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
1071
+ const project = await session2.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
1072
+ if (!project)
1073
+ throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
1074
+ return project;
1075
+ }
1076
+ static createEmpty() {
1077
+ return new _FlakinessConfig(ensureConfigPath(), {});
1078
+ }
1079
+ path() {
1080
+ return this._configPath;
1081
+ }
1082
+ projectPublicId() {
1083
+ return this._config.projectPublicId;
1084
+ }
1085
+ setProjectPublicId(projectId) {
1086
+ this._config.projectPublicId = projectId;
1087
+ }
1088
+ async save() {
1089
+ await fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
1090
+ await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
1091
+ }
1092
+ };
1093
+
1094
+ // src/flakinessSession.ts
1095
+ import fs3 from "fs/promises";
1096
+ import os2 from "os";
1097
+ import path3 from "path";
1098
+
487
1099
  // src/serverapi.ts
1100
+ import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
488
1101
  function createServerAPI(endpoint, options) {
489
1102
  endpoint += "/api/";
490
1103
  const fetcher = options?.auth ? (url, init) => fetch(url, {
@@ -501,24 +1114,30 @@ function createServerAPI(endpoint, options) {
501
1114
 
502
1115
  // src/flakinessSession.ts
503
1116
  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");
1117
+ 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
1118
  return configDir;
506
1119
  })();
507
- var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
1120
+ var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
508
1121
  var FlakinessSession = class _FlakinessSession {
509
1122
  constructor(_config) {
510
1123
  this._config = _config;
511
1124
  this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
512
1125
  }
1126
+ static async loadOrDie() {
1127
+ const session2 = await _FlakinessSession.load();
1128
+ if (!session2)
1129
+ throw new Error(`Please login first with 'npx flakiness login'`);
1130
+ return session2;
1131
+ }
513
1132
  static async load() {
514
- const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
1133
+ const data = await fs3.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
515
1134
  if (!data)
516
1135
  return void 0;
517
1136
  const json = JSON.parse(data);
518
1137
  return new _FlakinessSession(json);
519
1138
  }
520
1139
  static async remove() {
521
- await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
1140
+ await fs3.unlink(CONFIG_PATH).catch((e) => void 0);
522
1141
  }
523
1142
  api;
524
1143
  endpoint() {
@@ -531,21 +1150,21 @@ var FlakinessSession = class _FlakinessSession {
531
1150
  return this._config.token;
532
1151
  }
533
1152
  async save() {
534
- await fs2.mkdir(CONFIG_DIR, { recursive: true });
535
- await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
1153
+ await fs3.mkdir(CONFIG_DIR, { recursive: true });
1154
+ await fs3.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
536
1155
  }
537
1156
  };
538
1157
 
539
1158
  // src/cli/cmd-convert.ts
540
- import fs4 from "fs/promises";
541
- import path3 from "path";
1159
+ import fs5 from "fs/promises";
1160
+ import path5 from "path";
542
1161
 
543
1162
  // src/junit.ts
544
1163
  import { FlakinessReport as FK } from "@flakiness/report";
545
1164
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
546
1165
  import assert2 from "assert";
547
- import fs3 from "fs";
548
- import path2 from "path";
1166
+ import fs4 from "fs";
1167
+ import path4 from "path";
549
1168
  function getProperties(element) {
550
1169
  const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
551
1170
  if (!propertiesNodes.length)
@@ -587,8 +1206,8 @@ function extractStdout(testcase, stdio) {
587
1206
  }));
588
1207
  }
589
1208
  async function parseAttachment(value) {
590
- let absolutePath = path2.resolve(process.cwd(), value);
591
- if (fs3.existsSync(absolutePath)) {
1209
+ let absolutePath = path4.resolve(process.cwd(), value);
1210
+ if (fs4.existsSync(absolutePath)) {
592
1211
  const id = await sha1File(absolutePath);
593
1212
  return {
594
1213
  contentType: "image/png",
@@ -660,7 +1279,7 @@ async function traverseJUnitReport(context, node) {
660
1279
  id: attachment.id,
661
1280
  contentType: attachment.contentType,
662
1281
  //TODO: better default names for attachments?
663
- name: attachment.path ? path2.basename(attachment.path) : `attachment`
1282
+ name: attachment.path ? path4.basename(attachment.path) : `attachment`
664
1283
  });
665
1284
  } else {
666
1285
  annotations.push({
@@ -735,15 +1354,15 @@ async function parseJUnit(xmls, options) {
735
1354
 
736
1355
  // src/cli/cmd-convert.ts
737
1356
  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)) {
1357
+ const fullPath = path5.resolve(junitPath);
1358
+ if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
740
1359
  console.error(`Error: path ${fullPath} is not accessible`);
741
1360
  process.exit(1);
742
1361
  }
743
- const stat = await fs4.stat(fullPath);
1362
+ const stat = await fs5.stat(fullPath);
744
1363
  let xmlContents = [];
745
1364
  if (stat.isFile()) {
746
- const xmlContent = await fs4.readFile(fullPath, "utf-8");
1365
+ const xmlContent = await fs5.readFile(fullPath, "utf-8");
747
1366
  xmlContents.push(xmlContent);
748
1367
  } else if (stat.isDirectory()) {
749
1368
  const xmlFiles = await findXmlFiles(fullPath);
@@ -753,7 +1372,7 @@ async function cmdConvert(junitPath, options) {
753
1372
  }
754
1373
  console.log(`Found ${xmlFiles.length} XML files`);
755
1374
  for (const xmlFile of xmlFiles) {
756
- const xmlContent = await fs4.readFile(xmlFile, "utf-8");
1375
+ const xmlContent = await fs5.readFile(xmlFile, "utf-8");
757
1376
  xmlContents.push(xmlContent);
758
1377
  }
759
1378
  } else {
@@ -777,26 +1396,26 @@ async function cmdConvert(junitPath, options) {
777
1396
  runStartTimestamp: Date.now(),
778
1397
  runDuration: 0
779
1398
  });
780
- await fs4.writeFile("fkreport.json", JSON.stringify(report, null, 2));
1399
+ await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
781
1400
  console.log("\u2713 Saved report to fkreport.json");
782
1401
  if (attachments.length > 0) {
783
- await fs4.mkdir("fkattachments", { recursive: true });
1402
+ await fs5.mkdir("fkattachments", { recursive: true });
784
1403
  for (const attachment of attachments) {
785
1404
  if (attachment.path) {
786
- const destPath = path3.join("fkattachments", attachment.id);
787
- await fs4.copyFile(attachment.path, destPath);
1405
+ const destPath = path5.join("fkattachments", attachment.id);
1406
+ await fs5.copyFile(attachment.path, destPath);
788
1407
  } else if (attachment.body) {
789
- const destPath = path3.join("fkattachments", attachment.id);
790
- await fs4.writeFile(destPath, attachment.body);
1408
+ const destPath = path5.join("fkattachments", attachment.id);
1409
+ await fs5.writeFile(destPath, attachment.body);
791
1410
  }
792
1411
  }
793
1412
  console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
794
1413
  }
795
1414
  }
796
1415
  async function findXmlFiles(dir, result = []) {
797
- const entries = await fs4.readdir(dir, { withFileTypes: true });
1416
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
798
1417
  for (const entry of entries) {
799
- const fullPath = path3.join(dir, entry.name);
1418
+ const fullPath = path5.join(dir, entry.name);
800
1419
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
801
1420
  result.push(fullPath);
802
1421
  else if (entry.isDirectory())
@@ -807,53 +1426,8 @@ async function findXmlFiles(dir, result = []) {
807
1426
 
808
1427
  // src/cli/cmd-download.ts
809
1428
  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() });
1429
+ import path6 from "path";
1430
+ async function cmdDownload(session2, project, runId) {
857
1431
  const urls = await session2.api.run.downloadURLs.GET({
858
1432
  orgSlug: project.org.orgSlug,
859
1433
  projectSlug: project.projectSlug,
@@ -864,7 +1438,7 @@ async function cmdDownload(runId) {
864
1438
  console.log(`Directory ${rootDir} already exists!`);
865
1439
  return;
866
1440
  }
867
- const attachmentsDir = path5.join(rootDir, "attachments");
1441
+ const attachmentsDir = path6.join(rootDir, "attachments");
868
1442
  await fs6.promises.mkdir(rootDir, { recursive: true });
869
1443
  if (urls.attachmentURLs.length)
870
1444
  await fs6.promises.mkdir(attachmentsDir, { recursive: true });
@@ -872,7 +1446,7 @@ async function cmdDownload(runId) {
872
1446
  if (!response.ok)
873
1447
  throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
874
1448
  const reportContent = await response.text();
875
- await fs6.promises.writeFile(path5.join(rootDir, "report.json"), reportContent);
1449
+ await fs6.promises.writeFile(path6.join(rootDir, "report.json"), reportContent);
876
1450
  const attachmentDownloader = async () => {
877
1451
  while (urls.attachmentURLs.length) {
878
1452
  const url = urls.attachmentURLs.pop();
@@ -880,8 +1454,8 @@ async function cmdDownload(runId) {
880
1454
  if (!response2.ok)
881
1455
  throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
882
1456
  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);
1457
+ const filename = path6.basename(new URL(url).pathname);
1458
+ await fs6.promises.writeFile(path6.join(attachmentsDir, filename), fileBuffer);
885
1459
  }
886
1460
  };
887
1461
  const workerPromises = [];
@@ -907,11 +1481,10 @@ async function cmdLink(slug) {
907
1481
  console.log(`Failed to find project ${slug}`);
908
1482
  process.exit(1);
909
1483
  }
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()}`);
1484
+ const config = FlakinessConfig.createEmpty();
1485
+ config.setProjectPublicId(project.projectPublicId);
1486
+ await config.save();
1487
+ console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
915
1488
  }
916
1489
 
917
1490
  // ../server/lib/common/knownClientIds.js
@@ -921,6 +1494,7 @@ var KNOWN_CLIENT_IDS = {
921
1494
  };
922
1495
 
923
1496
  // src/cli/cmd-login.ts
1497
+ import open from "open";
924
1498
  import os3 from "os";
925
1499
  async function cmdLogin(endpoint) {
926
1500
  const api = createServerAPI(endpoint);
@@ -928,6 +1502,7 @@ async function cmdLogin(endpoint) {
928
1502
  clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
929
1503
  name: os3.hostname()
930
1504
  });
1505
+ await open(new URL(data.verificationUrl, endpoint).href);
931
1506
  console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
932
1507
  let token;
933
1508
  while (Date.now() < data.deadline) {
@@ -955,9 +1530,209 @@ async function cmdLogin(endpoint) {
955
1530
 
956
1531
  // src/cli/cmd-logout.ts
957
1532
  async function cmdLogout() {
1533
+ const session2 = await FlakinessSession.load();
1534
+ if (!session2)
1535
+ return;
1536
+ const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
1537
+ if (currentSession)
1538
+ await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
958
1539
  await FlakinessSession.remove();
959
1540
  }
960
1541
 
1542
+ // src/cli/cmd-serve.ts
1543
+ import open2 from "open";
1544
+ import path7 from "path";
1545
+
1546
+ // src/localReportServer.ts
1547
+ import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
1548
+ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
1549
+ import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
1550
+ import bodyParser from "body-parser";
1551
+ import compression from "compression";
1552
+ import debug from "debug";
1553
+ import express from "express";
1554
+ import "express-async-errors";
1555
+ import fs8 from "fs";
1556
+ import http2 from "http";
1557
+
1558
+ // src/localGit.ts
1559
+ import { exec } from "child_process";
1560
+ import { promisify } from "util";
1561
+ var execAsync = promisify(exec);
1562
+ async function listLocalCommits(gitRoot, head, count) {
1563
+ const FIELD_SEPARATOR = "|~|";
1564
+ const RECORD_SEPARATOR = "\0";
1565
+ const prettyFormat = [
1566
+ "%H",
1567
+ // %H: Full commit hash
1568
+ "%at",
1569
+ // %at: Author date as a Unix timestamp (seconds since epoch)
1570
+ "%an",
1571
+ // %an: Author name
1572
+ "%s"
1573
+ // %s: Subject (the first line of the commit message)
1574
+ ].join(FIELD_SEPARATOR);
1575
+ const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
1576
+ try {
1577
+ const { stdout } = await execAsync(command, { cwd: gitRoot });
1578
+ if (!stdout) {
1579
+ return [];
1580
+ }
1581
+ return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
1582
+ const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
1583
+ return {
1584
+ commitId,
1585
+ timestamp: parseInt(timestampStr, 10) * 1e3,
1586
+ // Convert timestamp from seconds to milliseconds
1587
+ author,
1588
+ message,
1589
+ walkIndex: 0
1590
+ };
1591
+ });
1592
+ } catch (error) {
1593
+ console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
1594
+ throw error;
1595
+ }
1596
+ }
1597
+
1598
+ // src/localReportApi.ts
1599
+ import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
1600
+ import fs7 from "fs";
1601
+ import { z } from "zod/v4";
1602
+ var t = TypedHTTP2.Router.create();
1603
+ var localReportRouter = {
1604
+ ping: t.get({
1605
+ handler: async () => {
1606
+ return "pong";
1607
+ }
1608
+ }),
1609
+ lastCommits: t.get({
1610
+ handler: async ({ ctx }) => {
1611
+ return ctx.commits;
1612
+ }
1613
+ }),
1614
+ report: {
1615
+ attachment: t.rawMethod("GET", {
1616
+ input: z.object({
1617
+ attachmentId: z.string().min(1).max(100).transform((id) => id)
1618
+ }),
1619
+ handler: async ({ ctx, input }) => {
1620
+ const idx = ctx.attachmentIdToPath.get(input.attachmentId);
1621
+ if (!idx)
1622
+ throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
1623
+ const buffer = await fs7.promises.readFile(idx.path);
1624
+ return TypedHTTP2.ok(buffer, idx.contentType);
1625
+ }
1626
+ }),
1627
+ json: t.get({
1628
+ handler: async ({ ctx }) => {
1629
+ return ctx.report;
1630
+ }
1631
+ })
1632
+ }
1633
+ };
1634
+
1635
+ // src/localReportServer.ts
1636
+ var logHTTPServer = debug("fk:http");
1637
+ var LocalReportServer = class _LocalReportServer {
1638
+ constructor(_server, _port, _authToken) {
1639
+ this._server = _server;
1640
+ this._port = _port;
1641
+ this._authToken = _authToken;
1642
+ }
1643
+ static async create(options) {
1644
+ const app = express();
1645
+ app.set("etag", false);
1646
+ const authToken = `fk-` + randomUUIDBase62();
1647
+ app.use(compression());
1648
+ app.use(bodyParser.json({ limit: 256 * 1024 }));
1649
+ app.use((req, res, next) => {
1650
+ if (!req.path.startsWith("/" + authToken))
1651
+ throw TypedHTTP3.HttpError.withCode("UNAUTHORIZED");
1652
+ res.setHeader("Access-Control-Allow-Headers", "*");
1653
+ res.setHeader("Access-Control-Allow-Origin", options.endpoint);
1654
+ res.setHeader("Access-Control-Allow-Methods", "*");
1655
+ if (req.method === "OPTIONS") {
1656
+ res.writeHead(204);
1657
+ res.end();
1658
+ return;
1659
+ }
1660
+ req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
1661
+ res.on("close", () => {
1662
+ if (!res.headersSent)
1663
+ logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
1664
+ });
1665
+ next();
1666
+ });
1667
+ app.use("/" + authToken, createTypedHttpExpressMiddleware({
1668
+ router: localReportRouter,
1669
+ createRootContext: async ({ req, res, input }) => {
1670
+ const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
1671
+ const attachmentsDir = options.attachmentsFolder;
1672
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1673
+ if (missingAttachments.length) {
1674
+ const first = missingAttachments.slice(0, 3);
1675
+ for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
1676
+ console.warn(`Missing attachment with id ${missingAttachments[i]}`);
1677
+ if (missingAttachments.length > 3)
1678
+ console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
1679
+ }
1680
+ return {
1681
+ report,
1682
+ commits: await listLocalCommits(process.cwd(), report.commitId, 100),
1683
+ attachmentIdToPath
1684
+ };
1685
+ }
1686
+ }));
1687
+ app.use((err, req, res, next) => {
1688
+ if (err instanceof TypedHTTP3.HttpError)
1689
+ return res.status(err.status).send({ error: err.message });
1690
+ logHTTPServer(err);
1691
+ res.status(500).send({ error: "Internal Server Error" });
1692
+ });
1693
+ const server = http2.createServer(app);
1694
+ server.on("error", (err) => {
1695
+ if (err.code === "ECONNRESET") {
1696
+ logHTTPServer("Client connection reset. Ignoring.");
1697
+ return;
1698
+ }
1699
+ throw err;
1700
+ });
1701
+ const port = await new Promise((resolve) => server.listen(options.port, () => {
1702
+ resolve(server.address().port);
1703
+ }));
1704
+ return new _LocalReportServer(server, port, authToken);
1705
+ }
1706
+ authToken() {
1707
+ return this._authToken;
1708
+ }
1709
+ port() {
1710
+ return this._port;
1711
+ }
1712
+ async dispose() {
1713
+ await new Promise((x) => this._server.close(x));
1714
+ }
1715
+ };
1716
+
1717
+ // src/cli/cmd-serve.ts
1718
+ async function cmdServe(reportFolder) {
1719
+ const reportPath = path7.join(reportFolder, "report.json");
1720
+ const session2 = await FlakinessSession.load();
1721
+ const config = await FlakinessConfig.load();
1722
+ const projectPublicId = config.projectPublicId();
1723
+ const project = projectPublicId && session2 ? await session2.api.project.getProject.GET({ projectPublicId }) : void 0;
1724
+ const endpoint = session2?.endpoint() ?? "https://flakiness.io";
1725
+ const server = await LocalReportServer.create({
1726
+ endpoint,
1727
+ port: 9373,
1728
+ reportPath,
1729
+ attachmentsFolder: reportFolder
1730
+ });
1731
+ const reportEndpoint = project ? `${endpoint}/localreport/${project.org.orgSlug}/${project.projectSlug}?port=${server.port()}&token=${server.authToken()}` : `${endpoint}/localreport?port=${server.port()}&token=${server.authToken()}`;
1732
+ console.log(`Navigate to ${reportEndpoint}`);
1733
+ await open2(reportEndpoint);
1734
+ }
1735
+
961
1736
  // src/cli/cmd-status.ts
962
1737
  async function cmdStatus() {
963
1738
  const session2 = await FlakinessSession.load();
@@ -967,31 +1742,34 @@ async function cmdStatus() {
967
1742
  }
968
1743
  const user = await session2.api.user.whoami.GET();
969
1744
  console.log(`user: ${user.userName} (${user.userLogin})`);
970
- const link = await FlakinessLink.load();
971
- if (!link) {
1745
+ const config = await FlakinessConfig.load();
1746
+ const projectPublicId = config.projectPublicId();
1747
+ if (!projectPublicId) {
972
1748
  console.log(`project: <not linked>`);
973
1749
  return;
974
1750
  }
975
- const project = await session2.api.project.getProject.GET({
976
- projectPublicId: link.projectId()
977
- });
1751
+ const project = await session2.api.project.getProject.GET({ projectPublicId });
978
1752
  console.log(`project: ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
979
1753
  }
980
1754
 
981
1755
  // src/cli/cmd-unlink.ts
982
1756
  async function cmdUnlink() {
983
- await FlakinessLink.remove();
1757
+ const config = await FlakinessConfig.load();
1758
+ if (!config.projectPublicId())
1759
+ return;
1760
+ config.setProjectPublicId(void 0);
1761
+ await config.save();
984
1762
  }
985
1763
 
986
1764
  // src/cli/cmd-upload-playwright-json.ts
987
- import fs8 from "fs/promises";
988
- import path6 from "path";
1765
+ import fs10 from "fs/promises";
1766
+ import path8 from "path";
989
1767
 
990
1768
  // src/playwrightJSONReport.ts
991
- import { FlakinessReport as FK2, FlakinessReport } from "@flakiness/report";
992
- import debug from "debug";
1769
+ import { FlakinessReport as FK2, FlakinessReport as FlakinessReport2 } from "@flakiness/report";
1770
+ import debug2 from "debug";
993
1771
  import { posix as posixPath2 } from "path";
994
- var dlog = debug("flakiness:json-report");
1772
+ var dlog = debug2("flakiness:json-report");
995
1773
  var PlaywrightJSONReport;
996
1774
  ((PlaywrightJSONReport2) => {
997
1775
  function collectMetadata(somePathInsideProject = process.cwd()) {
@@ -1023,7 +1801,7 @@ var PlaywrightJSONReport;
1023
1801
  };
1024
1802
  const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
1025
1803
  const report = {
1026
- category: FlakinessReport.CATEGORY_PLAYWRIGHT,
1804
+ category: FlakinessReport2.CATEGORY_PLAYWRIGHT,
1027
1805
  commitId: metadata.commitId,
1028
1806
  configPath,
1029
1807
  url: metadata.runURL,
@@ -1163,9 +1941,10 @@ function parseJSONError(context, error) {
1163
1941
  }
1164
1942
 
1165
1943
  // src/reportUploader.ts
1166
- import fs7 from "fs";
1944
+ import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
1945
+ import assert3 from "assert";
1946
+ import fs9 from "fs";
1167
1947
  import { URL as URL2 } from "url";
1168
- import { brotliCompressSync as brotliCompressSync2 } from "zlib";
1169
1948
  var ReportUploader = class _ReportUploader {
1170
1949
  static optionsFromEnv(overrides) {
1171
1950
  const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
@@ -1210,11 +1989,10 @@ var ReportUpload = class {
1210
1989
  this._options = options;
1211
1990
  this._report = report;
1212
1991
  this._attachments = attachments;
1213
- this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
1992
+ this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
1214
1993
  }
1215
1994
  async upload(options) {
1216
1995
  const response = await this._api.run.startUpload.POST({
1217
- flakinessAccessToken: this._options.flakinessAccessToken,
1218
1996
  attachmentIds: this._attachments.map((attachment) => attachment.id)
1219
1997
  }).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
1220
1998
  if (response?.error || !response.result)
@@ -1225,7 +2003,7 @@ var ReportUpload = class {
1225
2003
  const uploadURL = response.result.attachment_upload_urls[attachment.id];
1226
2004
  if (!uploadURL)
1227
2005
  throw new Error("Internal error: missing upload URL for attachment!");
1228
- return this._uploadAttachment(attachment, uploadURL);
2006
+ return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
1229
2007
  })
1230
2008
  ]);
1231
2009
  const response2 = await this._api.run.completeUpload.POST({
@@ -1235,7 +2013,7 @@ var ReportUpload = class {
1235
2013
  return { success: true, reportUrl: url };
1236
2014
  }
1237
2015
  async _uploadReport(data, uploadUrl, syncCompression) {
1238
- const compressed = syncCompression ? brotliCompressSync2(data) : await brotliCompressAsync(data);
2016
+ const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
1239
2017
  const headers = {
1240
2018
  "Content-Type": "application/json",
1241
2019
  "Content-Length": Buffer.byteLength(compressed) + "",
@@ -1252,11 +2030,34 @@ var ReportUpload = class {
1252
2030
  await responseDataPromise;
1253
2031
  }, HTTP_BACKOFF);
1254
2032
  }
1255
- async _uploadAttachment(attachment, uploadUrl) {
1256
- const bytesLength = attachment.path ? (await fs7.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
2033
+ async _uploadAttachment(attachment, uploadUrl, syncCompression) {
2034
+ const mimeType = attachment.contentType.toLocaleLowerCase().trim();
2035
+ const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
2036
+ if (!compressable && attachment.path) {
2037
+ const attachmentPath = attachment.path;
2038
+ await retryWithBackoff(async () => {
2039
+ const { request, responseDataPromise } = httpUtils.createRequest({
2040
+ url: uploadUrl,
2041
+ headers: {
2042
+ "Content-Type": attachment.contentType,
2043
+ "Content-Length": (await fs9.promises.stat(attachmentPath)).size + ""
2044
+ },
2045
+ method: "put"
2046
+ });
2047
+ fs9.createReadStream(attachmentPath).pipe(request);
2048
+ await responseDataPromise;
2049
+ }, HTTP_BACKOFF);
2050
+ return;
2051
+ }
2052
+ let buffer = attachment.body ? attachment.body : attachment.path ? await fs9.promises.readFile(attachment.path) : void 0;
2053
+ assert3(buffer);
2054
+ const encoding = compressable ? "br" : void 0;
2055
+ if (compressable)
2056
+ buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
1257
2057
  const headers = {
1258
2058
  "Content-Type": attachment.contentType,
1259
- "Content-Length": bytesLength + ""
2059
+ "Content-Length": Buffer.byteLength(buffer) + "",
2060
+ "Content-Encoding": encoding
1260
2061
  };
1261
2062
  await retryWithBackoff(async () => {
1262
2063
  const { request, responseDataPromise } = httpUtils.createRequest({
@@ -1264,13 +2065,8 @@ var ReportUpload = class {
1264
2065
  headers,
1265
2066
  method: "put"
1266
2067
  });
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
- }
2068
+ request.write(buffer);
2069
+ request.end();
1274
2070
  await responseDataPromise;
1275
2071
  }, HTTP_BACKOFF);
1276
2072
  }
@@ -1278,12 +2074,12 @@ var ReportUpload = class {
1278
2074
 
1279
2075
  // src/cli/cmd-upload-playwright-json.ts
1280
2076
  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)) {
2077
+ const fullPath = path8.resolve(relativePath);
2078
+ if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
1283
2079
  console.error(`Error: path ${fullPath} is not accessible`);
1284
2080
  process.exit(1);
1285
2081
  }
1286
- const text = await fs8.readFile(fullPath, "utf-8");
2082
+ const text = await fs10.readFile(fullPath, "utf-8");
1287
2083
  const playwrightJson = JSON.parse(text);
1288
2084
  const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
1289
2085
  extractAttachments: true
@@ -1304,38 +2100,18 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
1304
2100
  }
1305
2101
 
1306
2102
  // src/cli/cmd-upload.ts
1307
- import { FlakinessReport as FlakinessReport2 } from "@flakiness/report";
1308
- import fs9 from "fs/promises";
1309
- import path7 from "path";
2103
+ import fs11 from "fs/promises";
2104
+ import path9 from "path";
1310
2105
  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)) {
2106
+ const fullPath = path9.resolve(relativePath);
2107
+ if (!await fs11.access(fullPath, fs11.constants.F_OK).then(() => true).catch(() => false)) {
1313
2108
  console.error(`Error: path ${fullPath} is not accessible`);
1314
2109
  process.exit(1);
1315
2110
  }
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");
2111
+ const text = await fs11.readFile(fullPath, "utf-8");
1320
2112
  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
- });
2113
+ const attachmentsDir = options.attachmentsDir ?? path9.dirname(fullPath);
2114
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
1339
2115
  if (missingAttachments.length && !options.ignoreMissingAttachments) {
1340
2116
  console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
1341
2117
  process.exit(1);
@@ -1344,7 +2120,7 @@ async function cmdUpload(relativePath, options) {
1344
2120
  flakinessAccessToken: options.accessToken,
1345
2121
  flakinessEndpoint: options.endpoint
1346
2122
  });
1347
- const upload = uploader.createUpload(report, attachments);
2123
+ const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
1348
2124
  const uploadResult = await upload.upload();
1349
2125
  if (!uploadResult.success) {
1350
2126
  console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
@@ -1352,17 +2128,6 @@ async function cmdUpload(relativePath, options) {
1352
2128
  console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
1353
2129
  }
1354
2130
  }
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
2131
 
1367
2132
  // src/cli/cmd-whoami.ts
1368
2133
  async function cmdWhoami() {
@@ -1378,7 +2143,7 @@ async function cmdWhoami() {
1378
2143
 
1379
2144
  // src/cli/cli.ts
1380
2145
  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");
2146
+ var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
1382
2147
  var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
1383
2148
  var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
1384
2149
  var optIgnoreMissingAttachments = new Option("--ignore-missing-attachments", "Upload report even if some attachments are missing.").default("", "Same directory as the report file");
@@ -1388,20 +2153,42 @@ async function runCommand(callback) {
1388
2153
  } catch (e) {
1389
2154
  if (!(e instanceof Error))
1390
2155
  throw e;
1391
- if (process.env.DBG)
1392
- console.error(e.stack);
1393
- else
1394
- console.error(e.message);
2156
+ console.error(errorText(e));
1395
2157
  process.exit(1);
1396
2158
  }
1397
2159
  }
1398
- var PACKAGE_JSON = JSON.parse(fs10.readFileSync(path8.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
2160
+ var PACKAGE_JSON = JSON.parse(fs12.readFileSync(path10.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
1399
2161
  var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
2162
+ async function ensureAccessToken(options) {
2163
+ let accessToken = options.accessToken;
2164
+ if (!accessToken) {
2165
+ const config = await FlakinessConfig.load();
2166
+ const projectPublicId = config.projectPublicId();
2167
+ if (session && projectPublicId) {
2168
+ try {
2169
+ accessToken = (await session.api.project.getProject.GET({ projectPublicId })).readWriteAccessToken;
2170
+ } catch (e) {
2171
+ if (e instanceof TypedHTTP4.HttpError && e.status === 404) {
2172
+ } else {
2173
+ throw e;
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+ assert4(accessToken, `Please either pass FLAKINESS_ACCESS_TOKEN or run login + link`);
2179
+ return {
2180
+ ...options,
2181
+ accessToken
2182
+ };
2183
+ }
1400
2184
  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);
2185
+ await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
1402
2186
  }));
1403
- program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
2187
+ var optLink = new Option("--link <org/proj>", "A project to link to");
2188
+ program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
1404
2189
  await cmdLogin(options.endpoint);
2190
+ if (options.link)
2191
+ await cmdLink(options.link);
1405
2192
  }));
1406
2193
  program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
1407
2194
  await cmdLogout();
@@ -1418,14 +2205,46 @@ program.command("unlink").description("Unlink repository from the flakiness proj
1418
2205
  program.command("status").description("Status repository from the flakiness project").action(async () => runCommand(async () => {
1419
2206
  await cmdStatus();
1420
2207
  }));
1421
- program.command("download").description("Download run").argument("runId", "Run id to download").action(async (runId) => runCommand(async () => {
1422
- await cmdDownload(parseInt(runId, 10));
2208
+ var optRunId = new Option("--run-id <runId>", "RunId flakiness.io access token").argParser((value) => {
2209
+ const parsed = parseInt(value, 10);
2210
+ if (isNaN(parsed) || parsed < 1) {
2211
+ throw new Error("runId must be a number >= 1");
2212
+ }
2213
+ return parsed;
2214
+ });
2215
+ var optSince = new Option("--since <date>", "Start date for filtering").argParser((value) => {
2216
+ const parsed = new Date(value);
2217
+ if (isNaN(parsed.getTime())) {
2218
+ throw new Error("Invalid date format");
2219
+ }
2220
+ return parsed;
2221
+ });
2222
+ program.command("download").description("Download run").addOption(optSince).addOption(optRunId).action(async (options) => runCommand(async () => {
2223
+ const session2 = await FlakinessSession.loadOrDie();
2224
+ const project = await FlakinessConfig.projectOrDie(session2);
2225
+ let runIds = [];
2226
+ if (options.runId) {
2227
+ runIds = [options.runId, options.runId];
2228
+ } else if (options.since) {
2229
+ runIds = await session2.api.project.listRuns.GET({
2230
+ orgSlug: project.org.orgSlug,
2231
+ projectSlug: project.projectSlug,
2232
+ sinceTimestampMs: +options.since
2233
+ });
2234
+ console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
2235
+ }
2236
+ for (const runId of Ranges.iterate(runIds))
2237
+ await cmdDownload(session2, project, runId);
1423
2238
  }));
1424
2239
  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
2240
  await runCommand(async () => {
1426
- await cmdUpload(relativePath, options);
2241
+ await cmdUpload(relativePath, await ensureAccessToken(options));
1427
2242
  });
1428
2243
  });
2244
+ 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 () => {
2245
+ const dir = path10.join(process.cwd(), arg ?? "flakiness-report");
2246
+ await cmdServe(dir);
2247
+ }));
1429
2248
  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
2249
  await runCommand(async () => {
1431
2250
  await cmdConvert(junitPath, options);