repoburg 1.3.254 → 1.3.256
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/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
- package/agent-pane/frontend/dist/index.html +1 -1
- package/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
- package/visual-editor-proxy/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFloat64 = parseFloat64;
|
|
4
|
+
exports.atoi = atoi;
|
|
5
|
+
exports.parseDuration = parseDuration;
|
|
6
|
+
exports.parseRFC3339 = parseRFC3339;
|
|
7
|
+
exports.parseOffset = parseOffset;
|
|
8
|
+
exports.parseSpan = parseSpan;
|
|
9
|
+
exports.parseSince = parseSince;
|
|
10
|
+
exports.statusMatcher = statusMatcher;
|
|
11
|
+
/**
|
|
12
|
+
* Readers for tool parameters: replay offsets, window lengths, "since" and
|
|
13
|
+
* status filters. They accept what the Go server accepted, so the number and
|
|
14
|
+
* duration parsers follow Go's strconv.ParseFloat, strconv.Atoi and
|
|
15
|
+
* time.ParseDuration rather than JavaScript's Number().
|
|
16
|
+
*/
|
|
17
|
+
const gofmt_1 = require("./gofmt");
|
|
18
|
+
const CLOCK_RE = /^(?:(\d+):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?$/;
|
|
19
|
+
const MAX_INT64 = (1n << 63n) - 1n;
|
|
20
|
+
const MIN_INT64 = -(1n << 63n);
|
|
21
|
+
/**
|
|
22
|
+
* Go's int64(f) for a float, as arm64 does it: truncates toward zero,
|
|
23
|
+
* saturates out of range values and maps NaN to 0. Results beyond what a
|
|
24
|
+
* JavaScript number holds exactly are capped at Number.MAX_SAFE_INTEGER.
|
|
25
|
+
*/
|
|
26
|
+
function toInt64(f) {
|
|
27
|
+
if (Number.isNaN(f))
|
|
28
|
+
return 0;
|
|
29
|
+
const n = Math.trunc(f);
|
|
30
|
+
if (n > Number.MAX_SAFE_INTEGER)
|
|
31
|
+
return Number.MAX_SAFE_INTEGER;
|
|
32
|
+
if (n < -Number.MAX_SAFE_INTEGER)
|
|
33
|
+
return -Number.MAX_SAFE_INTEGER;
|
|
34
|
+
return n || 0; // no -0
|
|
35
|
+
}
|
|
36
|
+
/** The same conversion to int64 nanoseconds, exactly. */
|
|
37
|
+
function toInt64Big(f) {
|
|
38
|
+
if (Number.isNaN(f))
|
|
39
|
+
return 0n;
|
|
40
|
+
if (f >= 2 ** 63)
|
|
41
|
+
return MAX_INT64;
|
|
42
|
+
if (f <= -(2 ** 63))
|
|
43
|
+
return MIN_INT64;
|
|
44
|
+
return BigInt(Math.trunc(f));
|
|
45
|
+
}
|
|
46
|
+
// ---------- Go number syntax ----------
|
|
47
|
+
const DECIMAL_RE = /^[+-]?(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[eE][+-]?\d+(?:_\d+)*)?$/;
|
|
48
|
+
const HEX_RE = /^([+-]?)0[xX](?:_?([0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)(?:\.([0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)?)?|_?\.([0-9a-fA-F]+(?:_[0-9a-fA-F]+)*))[pP]([+-]?\d+(?:_\d+)*)$/;
|
|
49
|
+
const SPECIAL_RE = /^(?:[+-]?(?:inf|infinity)|nan)$/i;
|
|
50
|
+
/**
|
|
51
|
+
* Go's strconv.ParseFloat(s, 64): decimal and hexadecimal literals with
|
|
52
|
+
* optional underscores between digits, inf, infinity and nan. Returns null
|
|
53
|
+
* for a syntax error or an out of range value.
|
|
54
|
+
*/
|
|
55
|
+
function parseFloat64(s) {
|
|
56
|
+
if (SPECIAL_RE.test(s)) {
|
|
57
|
+
const t = s.toLowerCase();
|
|
58
|
+
if (t === 'nan')
|
|
59
|
+
return NaN;
|
|
60
|
+
return t[0] === '-' ? -Infinity : Infinity;
|
|
61
|
+
}
|
|
62
|
+
let f;
|
|
63
|
+
if (DECIMAL_RE.test(s)) {
|
|
64
|
+
f = Number(s.split('_').join(''));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const m = HEX_RE.exec(s);
|
|
68
|
+
if (!m)
|
|
69
|
+
return null;
|
|
70
|
+
const int = (m[2] ?? '').split('_').join('');
|
|
71
|
+
const frac = (m[3] ?? m[4] ?? '').split('_').join('');
|
|
72
|
+
const mant = int + frac;
|
|
73
|
+
const exp = Number(m[5].split('_').join('')) - 4 * frac.length;
|
|
74
|
+
f = Number(BigInt('0x' + (mant || '0'))) * 2 ** exp;
|
|
75
|
+
if (m[1] === '-')
|
|
76
|
+
f = -f;
|
|
77
|
+
}
|
|
78
|
+
return Number.isFinite(f) ? f : null; // overflow is Go's ErrRange
|
|
79
|
+
}
|
|
80
|
+
/** Go's strconv.Atoi: base 10, optional sign, int64 range. Returns null on error. */
|
|
81
|
+
function atoi(s) {
|
|
82
|
+
if (!/^[+-]?\d+$/.test(s))
|
|
83
|
+
return null;
|
|
84
|
+
const n = BigInt(s);
|
|
85
|
+
if (n > MAX_INT64 || n < MIN_INT64)
|
|
86
|
+
return null;
|
|
87
|
+
return Number(n);
|
|
88
|
+
}
|
|
89
|
+
// ---------- time.ParseDuration ----------
|
|
90
|
+
const UNITS = {
|
|
91
|
+
ns: 1n,
|
|
92
|
+
us: 1000n,
|
|
93
|
+
[String.fromCharCode(0xb5) + 's']: 1000n, // U+00B5 micro sign
|
|
94
|
+
[String.fromCharCode(0x3bc) + 's']: 1000n, // U+03BC Greek mu
|
|
95
|
+
ms: 1000000n,
|
|
96
|
+
s: 1000000000n,
|
|
97
|
+
m: 60000000000n,
|
|
98
|
+
h: 3600000000000n,
|
|
99
|
+
};
|
|
100
|
+
const TWO63 = 1n << 63n;
|
|
101
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
102
|
+
/**
|
|
103
|
+
* Go's time.ParseDuration: a signed sequence of decimal numbers with
|
|
104
|
+
* optional fractions and a unit each, like "300ms", "-1.5h" or "2h45m".
|
|
105
|
+
* Returns nanoseconds, or null when s is not a duration.
|
|
106
|
+
*/
|
|
107
|
+
function parseDuration(input) {
|
|
108
|
+
let s = input;
|
|
109
|
+
let neg = false;
|
|
110
|
+
if (s !== '' && (s[0] === '-' || s[0] === '+')) {
|
|
111
|
+
neg = s[0] === '-';
|
|
112
|
+
s = s.slice(1);
|
|
113
|
+
}
|
|
114
|
+
if (s === '0')
|
|
115
|
+
return 0n; // special case: no unit needed
|
|
116
|
+
if (s === '')
|
|
117
|
+
return null;
|
|
118
|
+
let d = 0n;
|
|
119
|
+
while (s !== '') {
|
|
120
|
+
// The next character must be [0-9.].
|
|
121
|
+
if (!(s[0] === '.' || isDigit(s[0])))
|
|
122
|
+
return null;
|
|
123
|
+
// [0-9]*
|
|
124
|
+
let i = 0;
|
|
125
|
+
let v = 0n;
|
|
126
|
+
while (i < s.length && isDigit(s[i])) {
|
|
127
|
+
if (v > TWO63 / 10n)
|
|
128
|
+
return null; // overflow
|
|
129
|
+
v = v * 10n + BigInt(s.charCodeAt(i) - 48);
|
|
130
|
+
if (v > TWO63)
|
|
131
|
+
return null;
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
const pre = i > 0;
|
|
135
|
+
s = s.slice(i);
|
|
136
|
+
// (\.[0-9]*)?
|
|
137
|
+
let f = 0n;
|
|
138
|
+
let scale = 1;
|
|
139
|
+
let post = false;
|
|
140
|
+
if (s !== '' && s[0] === '.') {
|
|
141
|
+
s = s.slice(1);
|
|
142
|
+
let j = 0;
|
|
143
|
+
let overflow = false;
|
|
144
|
+
while (j < s.length && isDigit(s[j])) {
|
|
145
|
+
if (!overflow) {
|
|
146
|
+
if (f > (TWO63 - 1n) / 10n) {
|
|
147
|
+
overflow = true; // stop accumulating precision
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const y = f * 10n + BigInt(s.charCodeAt(j) - 48);
|
|
151
|
+
if (y > TWO63)
|
|
152
|
+
overflow = true;
|
|
153
|
+
else {
|
|
154
|
+
f = y;
|
|
155
|
+
scale *= 10;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
j++;
|
|
160
|
+
}
|
|
161
|
+
post = j > 0;
|
|
162
|
+
s = s.slice(j);
|
|
163
|
+
}
|
|
164
|
+
if (!pre && !post)
|
|
165
|
+
return null; // no digits, e.g. ".s"
|
|
166
|
+
// The unit: everything up to the next number.
|
|
167
|
+
let k = 0;
|
|
168
|
+
while (k < s.length && !(s[k] === '.' || isDigit(s[k])))
|
|
169
|
+
k++;
|
|
170
|
+
if (k === 0)
|
|
171
|
+
return null; // missing unit
|
|
172
|
+
const unit = Object.prototype.hasOwnProperty.call(UNITS, s.slice(0, k)) ? UNITS[s.slice(0, k)] : undefined;
|
|
173
|
+
if (unit === undefined)
|
|
174
|
+
return null; // unknown unit
|
|
175
|
+
s = s.slice(k);
|
|
176
|
+
if (v > TWO63 / unit)
|
|
177
|
+
return null; // overflow
|
|
178
|
+
v *= unit;
|
|
179
|
+
if (f > 0n) {
|
|
180
|
+
// A float is nanosecond accurate for fractions of hours.
|
|
181
|
+
v += BigInt(Math.trunc(Number(f) * (Number(unit) / scale)));
|
|
182
|
+
if (v > TWO63)
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
d += v;
|
|
186
|
+
if (d > TWO63)
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (neg)
|
|
190
|
+
return -d;
|
|
191
|
+
if (d > TWO63 - 1n)
|
|
192
|
+
return null;
|
|
193
|
+
return d;
|
|
194
|
+
}
|
|
195
|
+
// ---------- RFC 3339 ----------
|
|
196
|
+
const RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{2}):(\d{2})(?:[.,](\d+))?(Z|([+-])(\d{2}):(\d{2}))$/;
|
|
197
|
+
function daysIn(month, year) {
|
|
198
|
+
if (month === 2)
|
|
199
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
200
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
201
|
+
}
|
|
202
|
+
/** Days since 1970-01-01 of a proleptic Gregorian date. */
|
|
203
|
+
function daysFromCivil(y, m, d) {
|
|
204
|
+
y -= m <= 2 ? 1 : 0;
|
|
205
|
+
const era = Math.floor(y / 400);
|
|
206
|
+
const yoe = y - era * 400;
|
|
207
|
+
const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
|
|
208
|
+
const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
|
|
209
|
+
return era * 146097 + doe - 719468;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Go's time.Parse(time.RFC3339, s), which also takes a one-digit hour, a
|
|
213
|
+
* comma before the fraction and zone offsets up to 24:60. Returns epoch
|
|
214
|
+
* milliseconds (Go's UnixMilli), or null.
|
|
215
|
+
*/
|
|
216
|
+
function parseRFC3339(s) {
|
|
217
|
+
const m = RFC3339_RE.exec(s);
|
|
218
|
+
if (!m)
|
|
219
|
+
return null;
|
|
220
|
+
const [year, month, day, hour, minute, second] = [1, 2, 3, 4, 5, 6].map((i) => Number(m[i]));
|
|
221
|
+
if (month < 1 || month > 12 || day < 1 || day > daysIn(month, year))
|
|
222
|
+
return null;
|
|
223
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
224
|
+
return null;
|
|
225
|
+
let offset = 0;
|
|
226
|
+
if (m[8] !== 'Z') {
|
|
227
|
+
const oh = Number(m[10]);
|
|
228
|
+
const om = Number(m[11]);
|
|
229
|
+
if (oh > 24 || om > 60)
|
|
230
|
+
return null;
|
|
231
|
+
offset = (oh * 60 + om) * 60 * (m[9] === '-' ? -1 : 1);
|
|
232
|
+
}
|
|
233
|
+
const millis = Number((m[7] ?? '').slice(0, 3).padEnd(3, '0'));
|
|
234
|
+
const secs = daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offset;
|
|
235
|
+
return secs * 1000 + millis;
|
|
236
|
+
}
|
|
237
|
+
// ---------- parameters ----------
|
|
238
|
+
/**
|
|
239
|
+
* Reads a position in a replay: "01:23", "01:23.4", "1:02:03", a duration
|
|
240
|
+
* ("83.4s", "1500ms", "2m") or a plain number of milliseconds. Empty means
|
|
241
|
+
* unset (-1).
|
|
242
|
+
*/
|
|
243
|
+
function parseOffset(input) {
|
|
244
|
+
const s = (0, gofmt_1.goTrimSpace)(input);
|
|
245
|
+
if (s === '')
|
|
246
|
+
return -1;
|
|
247
|
+
const m = CLOCK_RE.exec(s);
|
|
248
|
+
if (m) {
|
|
249
|
+
const h = Number(m[1] || '0');
|
|
250
|
+
const mins = Number(m[2]);
|
|
251
|
+
const sec = Number(m[3]);
|
|
252
|
+
const ms = Number((m[4] ?? '').padEnd(3, '0'));
|
|
253
|
+
return ((h * 60 + mins) * 60 + sec) * 1000 + ms;
|
|
254
|
+
}
|
|
255
|
+
const n = parseFloat64(s);
|
|
256
|
+
if (n !== null && n >= 0)
|
|
257
|
+
return toInt64(n);
|
|
258
|
+
const d = parseDuration(s);
|
|
259
|
+
if (d !== null && d >= 0n)
|
|
260
|
+
return Number(d / 1000000n);
|
|
261
|
+
throw new Error(`cannot read ${(0, gofmt_1.goQuote)(s)} as a time in the replay; use mm:ss(.mmm), a duration like 90s, or milliseconds`);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Reads a window length in milliseconds: a duration ("10s", "1500ms") or a
|
|
265
|
+
* plain number of seconds. Empty returns defMs.
|
|
266
|
+
*/
|
|
267
|
+
function parseSpan(input, defMs) {
|
|
268
|
+
const s = (0, gofmt_1.goTrimSpace)(input);
|
|
269
|
+
if (s === '')
|
|
270
|
+
return defMs;
|
|
271
|
+
const n = parseFloat64(s);
|
|
272
|
+
if (n !== null && n >= 0)
|
|
273
|
+
return toInt64(n * 1000);
|
|
274
|
+
const d = parseDuration(s);
|
|
275
|
+
if (d !== null && d >= 0n)
|
|
276
|
+
return Number(d / 1000000n);
|
|
277
|
+
throw new Error(`cannot read ${(0, gofmt_1.goQuote)(s)} as a duration; use e.g. 10s or 1500ms`);
|
|
278
|
+
}
|
|
279
|
+
/** Go's Time.UnixMilli of epoch nanoseconds: rounds down. */
|
|
280
|
+
function unixMilli(ns) {
|
|
281
|
+
const q = ns / 1000000n;
|
|
282
|
+
return Number(ns < 0n && q * 1000000n !== ns ? q - 1n : q);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Reads "2h", "30m", "3d" or an RFC 3339 time, relative to now (epoch ms).
|
|
286
|
+
* Returns epoch milliseconds, or null when s is empty.
|
|
287
|
+
*/
|
|
288
|
+
function parseSince(input, nowMs) {
|
|
289
|
+
const s = (0, gofmt_1.goTrimSpace)(input);
|
|
290
|
+
if (s === '')
|
|
291
|
+
return null;
|
|
292
|
+
const t = parseRFC3339(s);
|
|
293
|
+
if (t !== null)
|
|
294
|
+
return t;
|
|
295
|
+
const nowNs = BigInt(Math.trunc(nowMs)) * 1000000n;
|
|
296
|
+
if (s.endsWith('d')) {
|
|
297
|
+
const n = parseFloat64(s.slice(0, -1));
|
|
298
|
+
if (n !== null)
|
|
299
|
+
return unixMilli(nowNs - toInt64Big(n * 86400000000000));
|
|
300
|
+
}
|
|
301
|
+
const d = parseDuration(s);
|
|
302
|
+
if (d !== null)
|
|
303
|
+
return unixMilli(nowNs - d);
|
|
304
|
+
throw new Error(`cannot read since=${(0, gofmt_1.goQuote)(s)}; use e.g. 2h, 30m, 3d or an RFC 3339 time`);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Reads a status filter: "500", "5xx", ">=400", "failed" (status >= 400 or a
|
|
308
|
+
* transport error), "error" (transport errors only).
|
|
309
|
+
*/
|
|
310
|
+
function statusMatcher(input) {
|
|
311
|
+
const s = (0, gofmt_1.goToLower)((0, gofmt_1.goTrimSpace)(input));
|
|
312
|
+
if (s === '')
|
|
313
|
+
return () => true;
|
|
314
|
+
if (s === 'failed' || s === 'failures')
|
|
315
|
+
return (st, e) => e || st >= 400;
|
|
316
|
+
if (s === 'error' || s === 'errors')
|
|
317
|
+
return (_st, e) => e;
|
|
318
|
+
if (s.length === 3 && s.endsWith('xx') && s[0] >= '1' && s[0] <= '5') {
|
|
319
|
+
const base = Number(s[0]) * 100;
|
|
320
|
+
return (st) => st >= base && st < base + 100;
|
|
321
|
+
}
|
|
322
|
+
for (const op of ['>=', '<=', '>', '<', '=']) {
|
|
323
|
+
if (!s.startsWith(op))
|
|
324
|
+
continue;
|
|
325
|
+
const n = atoi((0, gofmt_1.goTrimSpace)(s.slice(op.length)));
|
|
326
|
+
if (n === null)
|
|
327
|
+
break;
|
|
328
|
+
switch (op) {
|
|
329
|
+
case '>=':
|
|
330
|
+
return (st) => st >= n;
|
|
331
|
+
case '<=':
|
|
332
|
+
return (st) => st > 0 && st <= n;
|
|
333
|
+
case '>':
|
|
334
|
+
return (st) => st > n;
|
|
335
|
+
case '<':
|
|
336
|
+
return (st) => st > 0 && st < n;
|
|
337
|
+
default:
|
|
338
|
+
return (st) => st === n;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const n = atoi(s);
|
|
342
|
+
if (n !== null)
|
|
343
|
+
return (st) => st === n;
|
|
344
|
+
throw new Error(`cannot read status=${(0, gofmt_1.goQuote)(s)}; use e.g. 500, 5xx, >=400, failed`);
|
|
345
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const gofmt_1 = require("./gofmt");
|
|
4
|
+
const params_1 = require("./params");
|
|
5
|
+
const ch = (code) => String.fromCharCode(code);
|
|
6
|
+
describe('parseOffset', () => {
|
|
7
|
+
it('reads clocks, durations and milliseconds', () => {
|
|
8
|
+
const cases = [
|
|
9
|
+
['', -1],
|
|
10
|
+
[' ', -1],
|
|
11
|
+
['01:23', 83000],
|
|
12
|
+
['01:23.4', 83400],
|
|
13
|
+
['1:02:03.004', 3723004],
|
|
14
|
+
['90s', 90000],
|
|
15
|
+
['1500ms', 1500],
|
|
16
|
+
['2500', 2500],
|
|
17
|
+
['2.5', 2],
|
|
18
|
+
['1_000', 1000],
|
|
19
|
+
['1m30s', 90000],
|
|
20
|
+
['-0', 0],
|
|
21
|
+
[' 00:05 ', 5000],
|
|
22
|
+
];
|
|
23
|
+
for (const [input, want] of cases)
|
|
24
|
+
expect([input, (0, params_1.parseOffset)(input)]).toEqual([input, want]);
|
|
25
|
+
});
|
|
26
|
+
it('rejects other text and negative times', () => {
|
|
27
|
+
expect(() => (0, params_1.parseOffset)('soon')).toThrow('cannot read "soon" as a time in the replay; use mm:ss(.mmm), a duration like 90s, or milliseconds');
|
|
28
|
+
for (const s of ['-1s', '-', '-5', '1:2:3:4', '00:60.1234'])
|
|
29
|
+
expect(() => (0, params_1.parseOffset)(s)).toThrow('cannot read');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('parseSpan and parseSince', () => {
|
|
33
|
+
it('reads spans in seconds or as durations', () => {
|
|
34
|
+
expect((0, params_1.parseSpan)('', 10000)).toBe(10000);
|
|
35
|
+
expect((0, params_1.parseSpan)('2.5', 0)).toBe(2500);
|
|
36
|
+
expect((0, params_1.parseSpan)('1500ms', 0)).toBe(1500);
|
|
37
|
+
expect((0, params_1.parseSpan)('0.1', 0)).toBe(100);
|
|
38
|
+
expect(() => (0, params_1.parseSpan)('-1', 0)).toThrow('cannot read "-1" as a duration; use e.g. 10s or 1500ms');
|
|
39
|
+
});
|
|
40
|
+
it('reads days, durations and RFC 3339 times', () => {
|
|
41
|
+
const now = Date.UTC(2026, 9, 6, 12, 0, 0);
|
|
42
|
+
expect((0, params_1.parseSince)('', now)).toBeNull();
|
|
43
|
+
expect((0, params_1.parseSince)('2d', now)).toBe(now - 48 * 3600000);
|
|
44
|
+
expect((0, params_1.parseSince)('1.5d', now)).toBe(now - 36 * 3600000);
|
|
45
|
+
expect((0, params_1.parseSince)('30m', now)).toBe(now - 30 * 60000);
|
|
46
|
+
expect((0, params_1.parseSince)('-1h', now)).toBe(now + 3600000);
|
|
47
|
+
expect((0, params_1.parseSince)('2026-10-06T10:00:00Z', now)).toBe(now - 2 * 3600000);
|
|
48
|
+
expect((0, params_1.parseSince)('2026-10-06T12:00:00+02:00', now)).toBe(now - 2 * 3600000);
|
|
49
|
+
expect((0, params_1.parseSince)('2026-10-06T10:00:00.25Z', now)).toBe(now - 2 * 3600000 + 250);
|
|
50
|
+
expect(() => (0, params_1.parseSince)('yesterday', now)).toThrow('cannot read since="yesterday"; use e.g. 2h, 30m, 3d or an RFC 3339 time');
|
|
51
|
+
});
|
|
52
|
+
it('parses RFC 3339 like Go', () => {
|
|
53
|
+
expect((0, params_1.parseRFC3339)('1970-01-01T00:00:00Z')).toBe(0);
|
|
54
|
+
expect((0, params_1.parseRFC3339)('1969-12-31T23:59:59.5Z')).toBe(-500);
|
|
55
|
+
expect((0, params_1.parseRFC3339)('2026-10-06T1:02:03Z')).toBe(Date.UTC(2026, 9, 6, 1, 2, 3)); // one-digit hour
|
|
56
|
+
expect((0, params_1.parseRFC3339)('2024-02-29T00:00:00Z')).toBe(Date.UTC(2024, 1, 29));
|
|
57
|
+
for (const bad of ['2026-02-29T00:00:00Z', '2026-10-06 12:00:00Z', '2026-10-06T24:00:00Z', '2026-10-06T12:00:00', '2026-10-06t12:00:00z', '26-10-06T12:00:00Z']) {
|
|
58
|
+
expect([bad, (0, params_1.parseRFC3339)(bad)]).toEqual([bad, null]);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('Go number parsing', () => {
|
|
63
|
+
it('parses durations like time.ParseDuration', () => {
|
|
64
|
+
const ms = 1000000n;
|
|
65
|
+
const cases = [
|
|
66
|
+
['0', 0n],
|
|
67
|
+
['-0', 0n],
|
|
68
|
+
['+5s', 5000n * ms],
|
|
69
|
+
['1h30m', 5400000n * ms],
|
|
70
|
+
['1500ms', 1500n * ms],
|
|
71
|
+
['2.5s', 2500n * ms],
|
|
72
|
+
['.5s', 500n * ms],
|
|
73
|
+
['5.s', 5000n * ms],
|
|
74
|
+
['1.5h', 5400000n * ms],
|
|
75
|
+
['-1.5h', -5400000n * ms],
|
|
76
|
+
['3us', 3000n],
|
|
77
|
+
['3' + ch(0xb5) + 's', 3000n],
|
|
78
|
+
['3' + ch(0x3bc) + 's', 3000n],
|
|
79
|
+
['9223372036854775807ns', 9223372036854775807n],
|
|
80
|
+
['-9223372036854775808ns', -9223372036854775808n],
|
|
81
|
+
['9223372036854775808ns', null],
|
|
82
|
+
['', null],
|
|
83
|
+
['-', null],
|
|
84
|
+
['1', null],
|
|
85
|
+
['1x', null],
|
|
86
|
+
['1h 30m', null],
|
|
87
|
+
['.s', null],
|
|
88
|
+
['1d', null],
|
|
89
|
+
];
|
|
90
|
+
for (const [input, want] of cases)
|
|
91
|
+
expect([input, (0, params_1.parseDuration)(input)]).toEqual([input, want]);
|
|
92
|
+
});
|
|
93
|
+
it('parses floats like strconv.ParseFloat', () => {
|
|
94
|
+
const cases = [
|
|
95
|
+
['1', 1],
|
|
96
|
+
['1.', 1],
|
|
97
|
+
['.5', 0.5],
|
|
98
|
+
['-2.5e3', -2500],
|
|
99
|
+
['1_000.5', 1000.5],
|
|
100
|
+
['0x1p4', 16],
|
|
101
|
+
['0x1.8p1', 3],
|
|
102
|
+
['inf', Infinity],
|
|
103
|
+
['-Infinity', -Infinity],
|
|
104
|
+
['1e400', null],
|
|
105
|
+
['0x10', null],
|
|
106
|
+
['_1', null],
|
|
107
|
+
['1__0', null],
|
|
108
|
+
['+nan', null],
|
|
109
|
+
['', null],
|
|
110
|
+
[' 1', null],
|
|
111
|
+
['0b101', null],
|
|
112
|
+
];
|
|
113
|
+
for (const [input, want] of cases)
|
|
114
|
+
expect([input, (0, params_1.parseFloat64)(input)]).toEqual([input, want]);
|
|
115
|
+
expect((0, params_1.parseFloat64)('NaN')).toBeNaN();
|
|
116
|
+
expect([(0, params_1.atoi)('+42'), (0, params_1.atoi)('-7'), (0, params_1.atoi)('4_2'), (0, params_1.atoi)('9223372036854775808'), (0, params_1.atoi)('')]).toEqual([42, -7, null, null, null]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('statusMatcher', () => {
|
|
120
|
+
it('matches statuses like the Go server', () => {
|
|
121
|
+
const cases = [
|
|
122
|
+
['failed', 500, false, true],
|
|
123
|
+
['failed', 0, true, true],
|
|
124
|
+
['failed', 200, false, false],
|
|
125
|
+
['5xx', 503, false, true],
|
|
126
|
+
['5XX', 404, false, false],
|
|
127
|
+
['>=400', 404, false, true],
|
|
128
|
+
['>= 400', 399, false, false],
|
|
129
|
+
['<500', 0, true, false],
|
|
130
|
+
['<500', 200, false, true],
|
|
131
|
+
['=204', 204, false, true],
|
|
132
|
+
['404', 404, false, true],
|
|
133
|
+
['error', 0, true, true],
|
|
134
|
+
['error', 500, false, false],
|
|
135
|
+
['', 0, false, true],
|
|
136
|
+
];
|
|
137
|
+
for (const [filter, status, err, want] of cases)
|
|
138
|
+
expect([filter, status, err, (0, params_1.statusMatcher)(filter)(status, err)]).toEqual([filter, status, err, want]);
|
|
139
|
+
expect(() => (0, params_1.statusMatcher)('>=abc')).toThrow('cannot read status=">=abc"; use e.g. 500, 5xx, >=400, failed');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('Go text helpers', () => {
|
|
143
|
+
it('quotes like strconv.Quote', () => {
|
|
144
|
+
const bs = '\\';
|
|
145
|
+
expect((0, gofmt_1.goQuote)('Acme "Shop"')).toBe('"Acme ' + bs + '"Shop' + bs + '""');
|
|
146
|
+
expect((0, gofmt_1.goQuote)('a' + bs + 'b\n\t' + ch(7) + ch(0))).toBe('"a' + bs + bs + 'b' + bs + 'n' + bs + 't' + bs + 'a' + bs + 'x00"');
|
|
147
|
+
expect((0, gofmt_1.goQuote)('Ünïcode 😀')).toBe('"Ünïcode 😀"');
|
|
148
|
+
expect((0, gofmt_1.goQuote)(ch(0xa0) + ch(0x200b))).toBe('"' + bs + 'u00a0' + bs + 'u200b"');
|
|
149
|
+
expect((0, gofmt_1.goQuote)(ch(0xd800))).toBe('"' + ch(0xfffd) + '"');
|
|
150
|
+
expect((0, gofmt_1.goQuote)('')).toBe('""');
|
|
151
|
+
});
|
|
152
|
+
it('pads, trims and lowers like Go', () => {
|
|
153
|
+
expect((0, gofmt_1.padRight)('click', 9)).toBe('click ');
|
|
154
|
+
expect((0, gofmt_1.padRight)('çğ', 4)).toBe('çğ ');
|
|
155
|
+
expect((0, gofmt_1.padRight)('toolongkind', 9)).toBe('toolongkind');
|
|
156
|
+
expect((0, gofmt_1.runeCount)('😀a')).toBe(2);
|
|
157
|
+
expect((0, gofmt_1.goTrimSpace)(ch(0x85) + ' x ' + ch(0x3000))).toBe('x');
|
|
158
|
+
expect((0, gofmt_1.goTrimSpace)(ch(0xfeff) + 'x')).toBe(ch(0xfeff) + 'x');
|
|
159
|
+
expect((0, gofmt_1.goToLower)(ch(0x130) + 'STANBUL')).toBe('istanbul');
|
|
160
|
+
expect((0, gofmt_1.goToLower)('ΟΔΟΣ')).toBe('οδοσ');
|
|
161
|
+
});
|
|
162
|
+
it('formats times in a zone like Go', () => {
|
|
163
|
+
const t0 = 1759650432000; // 2025-10-05 07:47:12 UTC
|
|
164
|
+
expect((0, gofmt_1.formatRFC3339)(t0, 'UTC')).toBe('2025-10-05T07:47:12Z');
|
|
165
|
+
expect((0, gofmt_1.formatRFC3339)(t0 + 999, 'Europe/Istanbul')).toBe('2025-10-05T10:47:12+03:00');
|
|
166
|
+
expect((0, gofmt_1.formatRFC3339)(t0, 'America/New_York')).toBe('2025-10-05T03:47:12-04:00');
|
|
167
|
+
expect((0, gofmt_1.formatRFC3339)(t0, 'Asia/Kolkata')).toBe('2025-10-05T13:17:12+05:30');
|
|
168
|
+
expect((0, gofmt_1.rfc3339DateTime)((0, gofmt_1.formatRFC3339)(t0, 'America/New_York'))).toBe('2025-10-05 03:47:12');
|
|
169
|
+
expect((0, gofmt_1.formatStamp)(t0, 'UTC')).toBe('Oct 5 07:47');
|
|
170
|
+
expect((0, gofmt_1.formatStamp)(t0 + 17 * 3600000, 'UTC')).toBe('Oct 6 00:47');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRecordLink = getRecordLink;
|
|
4
|
+
/**
|
|
5
|
+
* get_record_link hands out record links (see the /tabtape/record page):
|
|
6
|
+
* the start link carries the daemon's record token, which the extension
|
|
7
|
+
* checks before it records.
|
|
8
|
+
*/
|
|
9
|
+
const gofmt_1 = require("./gofmt");
|
|
10
|
+
function getRecordLink(opts, input) {
|
|
11
|
+
if (opts.recordToken === '')
|
|
12
|
+
throw new Error('record links are turned off in this Repoburg daemon');
|
|
13
|
+
const target = (0, gofmt_1.goTrimSpace)(input.url);
|
|
14
|
+
if (target !== '' && !recordableURL(target)) {
|
|
15
|
+
throw new Error(`url ${(0, gofmt_1.goQuote)(target)}: only http, https and file pages can be recorded`);
|
|
16
|
+
}
|
|
17
|
+
const start = [['token', opts.recordToken]];
|
|
18
|
+
const name = (0, gofmt_1.goTrimSpace)(input.name);
|
|
19
|
+
if (name !== '')
|
|
20
|
+
start.push(['name', name]);
|
|
21
|
+
if (target !== '')
|
|
22
|
+
start.push(['url', target]);
|
|
23
|
+
const out = {
|
|
24
|
+
start: `${opts.baseURL}/record?${encodeValues(start)}`,
|
|
25
|
+
stop: `${opts.baseURL}/record/stop?${encodeValues([['token', opts.recordToken]])}`,
|
|
26
|
+
};
|
|
27
|
+
let b = 'Open the start link in the browser to record (with Playwright MCP: browser_navigate). The tabtape extension must be loaded in that browser.\n\n';
|
|
28
|
+
b += `Start: ${out.start}\n`;
|
|
29
|
+
if (target !== '') {
|
|
30
|
+
b += `The tab then opens ${target} and is recorded from its first request.`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
b += 'The tab then says "Recording <session id>": open the page to test in that tab, it is recorded from its first request.';
|
|
34
|
+
}
|
|
35
|
+
b += ' If the tab stays on the start page ("Nothing happened" after 5 s), the extension is not loaded there and nothing is recorded.\n\n';
|
|
36
|
+
b += `Stop: ${out.stop}\n`;
|
|
37
|
+
b +=
|
|
38
|
+
'Open it in the same browser when you are done: the page names the session id and links the replay, for the user and for get_session_summary. Closing the tab also stops the recording.\n\n';
|
|
39
|
+
b += 'One recording runs at a time per browser: a new start link ends the previous one. The links stop working when the Repoburg daemon restarts.';
|
|
40
|
+
return { text: b, out };
|
|
41
|
+
}
|
|
42
|
+
/** Go's url.QueryEscape: only letters, digits and -_.~ stay; a space becomes "+". */
|
|
43
|
+
function queryEscape(s) {
|
|
44
|
+
return encodeURIComponent((0, gofmt_1.wellFormed)(s))
|
|
45
|
+
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
|
|
46
|
+
.replace(/%20/g, '+');
|
|
47
|
+
}
|
|
48
|
+
/** Go's url.Values.Encode: sorted by key. */
|
|
49
|
+
function encodeValues(values) {
|
|
50
|
+
return values
|
|
51
|
+
.slice()
|
|
52
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
53
|
+
.map(([k, v]) => `${queryEscape(k)}=${queryEscape(v)}`)
|
|
54
|
+
.join('&');
|
|
55
|
+
}
|
|
56
|
+
const RECORDABLE = new Set(['http', 'https', 'file']);
|
|
57
|
+
const BAD_ESCAPE = /%(?![0-9A-Fa-f]{2})/;
|
|
58
|
+
/** ASCII that Go's url.Parse rejects in a host. */
|
|
59
|
+
const BAD_HOST_CHAR = /[ \\^`{|}]/;
|
|
60
|
+
/** Go's url.getScheme: the scheme, "" when there is none, or null for a leading colon. */
|
|
61
|
+
function getScheme(raw) {
|
|
62
|
+
for (let i = 0; i < raw.length; i++) {
|
|
63
|
+
const c = raw[i];
|
|
64
|
+
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
|
|
65
|
+
continue;
|
|
66
|
+
if ((c >= '0' && c <= '9') || c === '+' || c === '-' || c === '.') {
|
|
67
|
+
if (i === 0)
|
|
68
|
+
return { scheme: '', rest: raw };
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === ':')
|
|
72
|
+
return i === 0 ? null : { scheme: raw.slice(0, i), rest: raw.slice(i + 1) };
|
|
73
|
+
return { scheme: '', rest: raw };
|
|
74
|
+
}
|
|
75
|
+
return { scheme: '', rest: raw };
|
|
76
|
+
}
|
|
77
|
+
/** Go's validOptionalPort. */
|
|
78
|
+
function validPort(port) {
|
|
79
|
+
return port === '' || /^:\d*$/.test(port);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether target is an http, https or file URL that Go's url.Parse accepts.
|
|
83
|
+
* The syntax checks cover control characters, percent escapes, ports, IPv6
|
|
84
|
+
* brackets and the characters a host cannot contain.
|
|
85
|
+
*/
|
|
86
|
+
function recordableURL(target) {
|
|
87
|
+
for (let i = 0; i < target.length; i++) {
|
|
88
|
+
const c = target.charCodeAt(i);
|
|
89
|
+
if (c < 0x20 || c === 0x7f)
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const hash = target.indexOf('#');
|
|
93
|
+
const u = hash < 0 ? target : target.slice(0, hash);
|
|
94
|
+
if (hash >= 0 && BAD_ESCAPE.test(target.slice(hash + 1)))
|
|
95
|
+
return false;
|
|
96
|
+
const parsed = getScheme(u);
|
|
97
|
+
if (!parsed || !RECORDABLE.has(parsed.scheme.toLowerCase()))
|
|
98
|
+
return false;
|
|
99
|
+
let rest = parsed.rest;
|
|
100
|
+
const q = rest.indexOf('?');
|
|
101
|
+
if (q >= 0)
|
|
102
|
+
rest = rest.slice(0, q);
|
|
103
|
+
if (rest.startsWith('//')) {
|
|
104
|
+
const slash = rest.indexOf('/', 2);
|
|
105
|
+
const authority = slash < 0 ? rest.slice(2) : rest.slice(2, slash);
|
|
106
|
+
rest = slash < 0 ? '' : rest.slice(slash);
|
|
107
|
+
const host = authority.slice(authority.lastIndexOf('@') + 1);
|
|
108
|
+
if (BAD_HOST_CHAR.test(host) || BAD_ESCAPE.test(host))
|
|
109
|
+
return false;
|
|
110
|
+
if (host.startsWith('[')) {
|
|
111
|
+
const end = host.indexOf(']');
|
|
112
|
+
if (end < 0 || !validPort(host.slice(end + 1)))
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const colon = host.lastIndexOf(':');
|
|
117
|
+
if (colon >= 0 && !validPort(host.slice(colon)))
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return !BAD_ESCAPE.test(rest);
|
|
122
|
+
}
|