ptywright 0.1.0 → 0.2.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.
Files changed (68) hide show
  1. package/README.md +459 -116
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-DIUx2w6X.mjs +3587 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-DzZlFrt1.mjs +1897 -0
  11. package/dist/runner-zApMYWZx.mjs +3257 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-VHuEWWj_.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +182 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/skills/ptywright-testing/SKILL.md +53 -33
  29. package/bin/ptywright +0 -4
  30. package/src/cli.ts +0 -414
  31. package/src/generator/doc_parser.ts +0 -341
  32. package/src/generator/generate.ts +0 -161
  33. package/src/generator/index.ts +0 -10
  34. package/src/generator/script_generator.ts +0 -209
  35. package/src/generator/step_extractor.ts +0 -397
  36. package/src/mcp/http_server.ts +0 -174
  37. package/src/mcp/script_recording.ts +0 -238
  38. package/src/mcp/server.ts +0 -1348
  39. package/src/pty/bun_pty_adapter.ts +0 -34
  40. package/src/pty/bun_terminal_adapter.ts +0 -149
  41. package/src/pty/pty_adapter.ts +0 -31
  42. package/src/script/dsl.ts +0 -188
  43. package/src/script/module.ts +0 -43
  44. package/src/script/path.ts +0 -151
  45. package/src/script/run.ts +0 -108
  46. package/src/script/run_all.ts +0 -229
  47. package/src/script/runner.ts +0 -983
  48. package/src/script/schema.ts +0 -237
  49. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  50. package/src/script/steps/index.ts +0 -2
  51. package/src/script/suite_report.ts +0 -626
  52. package/src/session/session_manager.ts +0 -145
  53. package/src/session/terminal_session.ts +0 -473
  54. package/src/terminal/ansi.ts +0 -142
  55. package/src/terminal/keys.ts +0 -180
  56. package/src/terminal/mask.ts +0 -70
  57. package/src/terminal/mouse.ts +0 -75
  58. package/src/terminal/snapshot.ts +0 -196
  59. package/src/terminal/style.ts +0 -121
  60. package/src/terminal/view.ts +0 -49
  61. package/src/trace/asciicast.ts +0 -20
  62. package/src/trace/asciinema_player_assets.ts +0 -44
  63. package/src/trace/cast_to_txt.ts +0 -116
  64. package/src/trace/recorder.ts +0 -110
  65. package/src/trace/report.ts +0 -2092
  66. package/src/types.ts +0 -86
  67. package/src/util/hash.ts +0 -8
  68. package/src/util/sleep.ts +0 -5
@@ -0,0 +1,893 @@
1
+ import { Terminal } from "@xterm/headless";
2
+ //#region src/terminal/keys.ts
3
+ const CSI = "\x1B[";
4
+ const SS3 = "\x1BO";
5
+ function ctrlChar(letter) {
6
+ const code = letter.toUpperCase().charCodeAt(0);
7
+ if (code < 65 || code > 90) throw new Error(`Unsupported ctrl key: ${letter}`);
8
+ return String.fromCharCode(code - 64);
9
+ }
10
+ function encodeKey(key) {
11
+ const normalized = key.trim();
12
+ if (normalized.length === 1) return normalized;
13
+ const ctrlMatch = /^c-(.)$/i.exec(normalized);
14
+ if (ctrlMatch) return ctrlChar(ctrlMatch[1] ?? "");
15
+ const parsed = parseKeySpec(normalized);
16
+ const mod = modifierParam(parsed.modifiers);
17
+ const keyName = parsed.key.toLowerCase();
18
+ if (keyName === "enter" || keyName === "return") return "\r";
19
+ if (keyName === "esc" || keyName === "escape") return "\x1B";
20
+ if (keyName === "backspace") return "";
21
+ if (keyName === "space") return " ";
22
+ if (keyName === "tab") return parsed.modifiers.shift ? `${CSI}Z` : " ";
23
+ if (keyName === "backtab" || keyName === "btab") return `${CSI}Z`;
24
+ const arrowFinal = arrowKeyFinal(keyName);
25
+ if (arrowFinal) {
26
+ if (mod === null) return `${CSI}${arrowFinal}`;
27
+ return `${CSI}1;${mod}${arrowFinal}`;
28
+ }
29
+ const homeEndFinal = homeEndFinalChar(keyName);
30
+ if (homeEndFinal) {
31
+ if (mod === null) return `${CSI}${homeEndFinal}`;
32
+ return `${CSI}1;${mod}${homeEndFinal}`;
33
+ }
34
+ const tildeCode = tildeKeyCode(keyName);
35
+ if (tildeCode) {
36
+ if (mod === null) return `${CSI}${tildeCode}~`;
37
+ return `${CSI}${tildeCode};${mod}~`;
38
+ }
39
+ const functionKey = functionKeySpec(keyName);
40
+ if (functionKey) {
41
+ if (mod === null) return functionKey.base;
42
+ return functionKey.modified(mod);
43
+ }
44
+ if (parsed.key.length === 1) {
45
+ let outChar = parsed.key;
46
+ if (parsed.modifiers.shift && /^[a-z]$/i.test(outChar)) outChar = outChar.toUpperCase();
47
+ let encoded = outChar;
48
+ if (parsed.modifiers.ctrl && /^[a-z]$/i.test(outChar)) encoded = ctrlChar(outChar);
49
+ if (parsed.modifiers.alt) encoded = `\x1b${encoded}`;
50
+ return encoded;
51
+ }
52
+ throw new Error(`Unsupported key: ${key}`);
53
+ }
54
+ function parseKeySpec(spec) {
55
+ const tokens = spec.trim().split(/[+-]/g).map((t) => t.trim()).filter(Boolean);
56
+ const modifiers = {
57
+ shift: false,
58
+ alt: false,
59
+ ctrl: false
60
+ };
61
+ const keys = [];
62
+ for (const token of tokens) {
63
+ const lower = token.toLowerCase();
64
+ if (lower === "shift") {
65
+ modifiers.shift = true;
66
+ continue;
67
+ }
68
+ if (lower === "alt" || lower === "meta") {
69
+ modifiers.alt = true;
70
+ continue;
71
+ }
72
+ if (lower === "ctrl" || lower === "control") {
73
+ modifiers.ctrl = true;
74
+ continue;
75
+ }
76
+ keys.push(token);
77
+ }
78
+ const key = keys.join("+").trim();
79
+ if (!key) throw new Error(`Unsupported key: ${spec}`);
80
+ return {
81
+ modifiers,
82
+ key
83
+ };
84
+ }
85
+ function modifierParam(mods) {
86
+ if (!mods.shift && !mods.alt && !mods.ctrl) return null;
87
+ return 1 + (mods.shift ? 1 : 0) + (mods.alt ? 2 : 0) + (mods.ctrl ? 4 : 0);
88
+ }
89
+ function arrowKeyFinal(key) {
90
+ if (key === "up") return "A";
91
+ if (key === "down") return "B";
92
+ if (key === "right") return "C";
93
+ if (key === "left") return "D";
94
+ return null;
95
+ }
96
+ function homeEndFinalChar(key) {
97
+ if (key === "home") return "H";
98
+ if (key === "end") return "F";
99
+ return null;
100
+ }
101
+ function tildeKeyCode(key) {
102
+ if (key === "pageup" || key === "pgup") return "5";
103
+ if (key === "pagedown" || key === "pgdn") return "6";
104
+ if (key === "insert" || key === "ins") return "2";
105
+ if (key === "delete" || key === "del") return "3";
106
+ return null;
107
+ }
108
+ function functionKeySpec(key) {
109
+ const match = /^f(\d{1,2})$/.exec(key);
110
+ if (!match) return null;
111
+ const n = Number(match[1]);
112
+ if (!Number.isFinite(n) || n < 1 || n > 12) return null;
113
+ if (n === 1) return {
114
+ base: `${SS3}P`,
115
+ modified: (m) => `${CSI}1;${m}P`
116
+ };
117
+ if (n === 2) return {
118
+ base: `${SS3}Q`,
119
+ modified: (m) => `${CSI}1;${m}Q`
120
+ };
121
+ if (n === 3) return {
122
+ base: `${SS3}R`,
123
+ modified: (m) => `${CSI}1;${m}R`
124
+ };
125
+ if (n === 4) return {
126
+ base: `${SS3}S`,
127
+ modified: (m) => `${CSI}1;${m}S`
128
+ };
129
+ const code = {
130
+ 5: "15",
131
+ 6: "17",
132
+ 7: "18",
133
+ 8: "19",
134
+ 9: "20",
135
+ 10: "21",
136
+ 11: "23",
137
+ 12: "24"
138
+ }[n];
139
+ if (!code) return null;
140
+ return {
141
+ base: `${CSI}${code}~`,
142
+ modified: (m) => `${CSI}${code};${m}~`
143
+ };
144
+ }
145
+ //#endregion
146
+ //#region src/terminal/style.ts
147
+ const DEFAULT_STYLE = {
148
+ fg: { mode: "default" },
149
+ bg: { mode: "default" },
150
+ bold: false,
151
+ dim: false,
152
+ italic: false,
153
+ underline: false,
154
+ inverse: false,
155
+ strikethrough: false
156
+ };
157
+ function extractStyle(cell) {
158
+ const style = {
159
+ fg: extractColor(cell.isFgDefault(), cell.isFgPalette(), cell.isFgRGB(), cell.getFgColor()),
160
+ bg: extractColor(cell.isBgDefault(), cell.isBgPalette(), cell.isBgRGB(), cell.getBgColor()),
161
+ bold: cell.isBold() !== 0,
162
+ dim: cell.isDim() !== 0,
163
+ italic: cell.isItalic() !== 0,
164
+ underline: cell.isUnderline() !== 0,
165
+ inverse: cell.isInverse() !== 0,
166
+ strikethrough: cell.isStrikethrough() !== 0
167
+ };
168
+ return isDefaultStyle(style) ? DEFAULT_STYLE : style;
169
+ }
170
+ function isDefaultStyle(style) {
171
+ return style.fg.mode === "default" && style.bg.mode === "default" && !style.bold && !style.dim && !style.italic && !style.underline && !style.inverse && !style.strikethrough;
172
+ }
173
+ function styleKey(style) {
174
+ return [
175
+ style.fg.mode === "default" ? "d" : `${style.fg.mode}:${style.fg.value}`,
176
+ style.bg.mode === "default" ? "d" : `${style.bg.mode}:${style.bg.value}`,
177
+ style.bold ? "b" : "",
178
+ style.dim ? "d" : "",
179
+ style.italic ? "i" : "",
180
+ style.underline ? "u" : "",
181
+ style.inverse ? "r" : "",
182
+ style.strikethrough ? "s" : ""
183
+ ].join("|");
184
+ }
185
+ function findMeaningfulEndCol(line, cols, nullCell) {
186
+ if (!line) return 0;
187
+ for (let x = cols - 1; x >= 0; x -= 1) {
188
+ const cell = line.getCell(x, nullCell);
189
+ if (!cell) continue;
190
+ if (cell.getWidth() === 0) continue;
191
+ const chars = cell.getChars();
192
+ const style = extractStyle(cell);
193
+ if (chars !== "" && chars !== " " || !isDefaultStyle(style)) return x + 1;
194
+ }
195
+ return 0;
196
+ }
197
+ function extractColor(isDefault, isPalette, isRgb, value) {
198
+ if (isDefault) return { mode: "default" };
199
+ if (isRgb) return {
200
+ mode: "rgb",
201
+ value
202
+ };
203
+ if (isPalette) return {
204
+ mode: "palette",
205
+ value
206
+ };
207
+ return { mode: "default" };
208
+ }
209
+ //#endregion
210
+ //#region src/terminal/ansi.ts
211
+ const ESC$2 = "\x1B";
212
+ const SGR_RESET = `${ESC$2}[0m`;
213
+ function renderAnsiLines(terminal, options) {
214
+ const scope = options?.scope ?? "visible";
215
+ const trimRight = options?.trimRight ?? false;
216
+ const buffer = terminal.buffer.active;
217
+ const nullCell = buffer.getNullCell();
218
+ let startY = 0;
219
+ let count = buffer.length;
220
+ if (scope === "visible") {
221
+ startY = buffer.viewportY;
222
+ count = terminal.rows;
223
+ }
224
+ const out = [];
225
+ for (let i = 0; i < count; i += 1) {
226
+ const y = startY + i;
227
+ const line = buffer.getLine(y);
228
+ out.push(renderLine(line, terminal.cols, nullCell, trimRight));
229
+ }
230
+ return out;
231
+ }
232
+ function renderLine(line, cols, nullCell, trimRight) {
233
+ let endCol = cols;
234
+ if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
235
+ let ansi = "";
236
+ let plain = "";
237
+ let hasStyle = false;
238
+ let usedSgr = false;
239
+ const defaultKey = styleKey(DEFAULT_STYLE);
240
+ let currentKey = defaultKey;
241
+ for (let x = 0; x < endCol; x += 1) {
242
+ const cell = line?.getCell(x, nullCell);
243
+ if (!cell) {
244
+ plain += " ";
245
+ ansi += " ";
246
+ continue;
247
+ }
248
+ if (cell.getWidth() === 0) continue;
249
+ const chars = cell.getChars() || " ";
250
+ const style = extractStyle(cell);
251
+ const isDefault = isDefaultStyle(style);
252
+ if (!isDefault) hasStyle = true;
253
+ const key = styleKey(style);
254
+ if (key !== currentKey) {
255
+ ansi += isDefault ? SGR_RESET : toSgr(style);
256
+ usedSgr = true;
257
+ currentKey = key;
258
+ }
259
+ ansi += chars;
260
+ plain += chars;
261
+ }
262
+ if (usedSgr && currentKey !== defaultKey) ansi += SGR_RESET;
263
+ return {
264
+ ansi,
265
+ plain,
266
+ hasStyle
267
+ };
268
+ }
269
+ function toSgr(style) {
270
+ const codes = ["0"];
271
+ if (style.bold) codes.push("1");
272
+ if (style.dim) codes.push("2");
273
+ if (style.italic) codes.push("3");
274
+ if (style.underline) codes.push("4");
275
+ if (style.inverse) codes.push("7");
276
+ if (style.strikethrough) codes.push("9");
277
+ if (style.fg.mode === "palette") codes.push(`38;5;${style.fg.value}`);
278
+ else if (style.fg.mode === "rgb") {
279
+ const [r, g, b] = rgb(style.fg.value);
280
+ codes.push(`38;2;${r};${g};${b}`);
281
+ }
282
+ if (style.bg.mode === "palette") codes.push(`48;5;${style.bg.value}`);
283
+ else if (style.bg.mode === "rgb") {
284
+ const [r, g, b] = rgb(style.bg.value);
285
+ codes.push(`48;2;${r};${g};${b}`);
286
+ }
287
+ return `${ESC$2}[${codes.join(";")}m`;
288
+ }
289
+ function rgb(value) {
290
+ return [
291
+ value >> 16 & 255,
292
+ value >> 8 & 255,
293
+ value & 255
294
+ ];
295
+ }
296
+ //#endregion
297
+ //#region src/terminal/mouse.ts
298
+ const ESC$1 = "\x1B";
299
+ function encodeSgrMouse(event) {
300
+ const x = clampInt(event.x, 1, 500);
301
+ const y = clampInt(event.y, 1, 300);
302
+ if (event.action === "click") return `${encodeSingleSgrMouse({
303
+ ...event,
304
+ action: "down"
305
+ }, x, y)}${encodeSingleSgrMouse({
306
+ ...event,
307
+ action: "up"
308
+ }, x, y)}`;
309
+ return encodeSingleSgrMouse(event, x, y);
310
+ }
311
+ function encodeSingleSgrMouse(event, x, y) {
312
+ const modifiers = event.modifiers;
313
+ const modifierBits = (modifiers?.shift ? 4 : 0) + (modifiers?.alt ? 8 : 0) + (modifiers?.ctrl ? 16 : 0);
314
+ const buttonCode = buttonToCode(event.button ?? "left");
315
+ if (event.action === "down") return `${ESC$1}[<${buttonCode + modifierBits};${x};${y}M`;
316
+ if (event.action === "up") return `${ESC$1}[<${buttonCode + modifierBits};${x};${y}m`;
317
+ if (event.action === "scroll_up") return `${ESC$1}[<${64 + modifierBits};${x};${y}M`;
318
+ if (event.action === "scroll_down") return `${ESC$1}[<${65 + modifierBits};${x};${y}M`;
319
+ return `${ESC$1}[<${32 + (event.button ? buttonCode : 3) + modifierBits};${x};${y}M`;
320
+ }
321
+ function buttonToCode(button) {
322
+ if (button === "left") return 0;
323
+ if (button === "middle") return 1;
324
+ return 2;
325
+ }
326
+ function clampInt(value, min, max) {
327
+ if (!Number.isFinite(value)) return min;
328
+ const int = Math.trunc(value);
329
+ if (int < min) return min;
330
+ if (int > max) return max;
331
+ return int;
332
+ }
333
+ //#endregion
334
+ //#region src/terminal/snapshot.ts
335
+ function snapshotVisibleLines(terminal, trimRight) {
336
+ const buffer = terminal.buffer.active;
337
+ const nullCell = buffer.getNullCell();
338
+ const startY = buffer.viewportY;
339
+ const lines = [];
340
+ for (let row = 0; row < terminal.rows; row += 1) {
341
+ const line = buffer.getLine(startY + row);
342
+ lines.push(snapshotPlainLine(line, terminal.cols, nullCell, trimRight));
343
+ }
344
+ return lines;
345
+ }
346
+ function snapshotVisibleStyleRuns(terminal, trimRight) {
347
+ const buffer = terminal.buffer.active;
348
+ const nullCell = buffer.getNullCell();
349
+ const startY = buffer.viewportY;
350
+ const out = [];
351
+ for (let row = 0; row < terminal.rows; row += 1) {
352
+ const line = buffer.getLine(startY + row);
353
+ out.push(snapshotStyleRuns(line, terminal.cols, nullCell, trimRight));
354
+ }
355
+ return out;
356
+ }
357
+ function snapshotBufferLines(terminal, trimRight) {
358
+ const buffer = terminal.buffer.active;
359
+ const lines = [];
360
+ for (let y = 0; y < buffer.length; y += 1) {
361
+ const line = buffer.getLine(y);
362
+ lines.push(line ? line.translateToString(trimRight, 0, terminal.cols) : "");
363
+ }
364
+ return lines;
365
+ }
366
+ function snapshotLines(terminal, options) {
367
+ const trimRight = options?.trimRight ?? true;
368
+ return (options?.scope ?? "visible") === "buffer" ? snapshotBufferLines(terminal, trimRight) : snapshotVisibleLines(terminal, trimRight);
369
+ }
370
+ function snapshotGrid(terminal, options) {
371
+ const trimRight = options?.trimRight ?? true;
372
+ const includeStyles = options?.includeStyles ?? false;
373
+ const buffer = terminal.buffer.active;
374
+ const lines = snapshotVisibleLines(terminal, trimRight);
375
+ return {
376
+ cols: terminal.cols,
377
+ rows: terminal.rows,
378
+ bufferType: buffer.type,
379
+ cursorX: buffer.cursorX,
380
+ cursorY: buffer.cursorY,
381
+ viewportY: buffer.viewportY,
382
+ lines,
383
+ styleRuns: includeStyles ? snapshotVisibleStyleRuns(terminal, trimRight) : void 0
384
+ };
385
+ }
386
+ function snapshotPlainLine(line, cols, nullCell, trimRight) {
387
+ let endCol = cols;
388
+ if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
389
+ let out = "";
390
+ for (let x = 0; x < endCol; x += 1) {
391
+ const cell = line?.getCell(x, nullCell);
392
+ if (!cell) {
393
+ out += " ";
394
+ continue;
395
+ }
396
+ if (cell.getWidth() === 0) continue;
397
+ out += cell.getChars() || " ";
398
+ }
399
+ return out;
400
+ }
401
+ function snapshotStyleRuns(line, cols, nullCell, trimRight) {
402
+ let endCol = cols;
403
+ if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
404
+ const out = [];
405
+ let currentKey = null;
406
+ let currentRun = null;
407
+ for (let x = 0; x < endCol; x += 1) {
408
+ const cell = line?.getCell(x, nullCell);
409
+ if (!cell) {
410
+ if (currentRun) {
411
+ out.push(currentRun);
412
+ currentRun = null;
413
+ currentKey = null;
414
+ }
415
+ continue;
416
+ }
417
+ const width = cell.getWidth();
418
+ if (width === 0) continue;
419
+ const style = extractStyle(cell);
420
+ if (isDefaultStyle(style)) {
421
+ if (currentRun) {
422
+ out.push(currentRun);
423
+ currentRun = null;
424
+ currentKey = null;
425
+ }
426
+ continue;
427
+ }
428
+ const key = styleKey(style);
429
+ if (currentRun && key === currentKey) {
430
+ currentRun.endCol = x + width;
431
+ continue;
432
+ }
433
+ if (currentRun) out.push(currentRun);
434
+ currentKey = key;
435
+ currentRun = {
436
+ startCol: x,
437
+ endCol: x + width,
438
+ style
439
+ };
440
+ }
441
+ if (currentRun) out.push(currentRun);
442
+ return out;
443
+ }
444
+ //#endregion
445
+ //#region src/terminal/mask.ts
446
+ function applyTextMaskRules(lines, rules) {
447
+ if (!rules || rules.length === 0) return lines;
448
+ const compiled = compileMaskRules(rules);
449
+ if (compiled.length === 0) return lines;
450
+ return lines.map((line) => applyCompiledMaskRules(line, compiled));
451
+ }
452
+ function compileMaskRules(rules) {
453
+ const compiled = [];
454
+ for (const rule of rules) {
455
+ if (!rule.regex.trim()) continue;
456
+ const preserveLength = rule.preserveLength ?? false;
457
+ const replacement = preserveLength ? firstChar(rule.replacement) : rule.replacement ?? "<masked>";
458
+ const flags = normalizeFlags(rule.flags);
459
+ let regex;
460
+ try {
461
+ regex = new RegExp(rule.regex, flags);
462
+ } catch (error) {
463
+ throw new Error(`invalid mask rule regex=${JSON.stringify(rule.regex)} flags=${JSON.stringify(rule.flags ?? "")}: ${error.message}`);
464
+ }
465
+ compiled.push({
466
+ regex,
467
+ replacement,
468
+ preserveLength
469
+ });
470
+ }
471
+ return compiled;
472
+ }
473
+ function normalizeFlags(flags) {
474
+ const value = flags?.trim() ? flags.trim() : "g";
475
+ const set = new Set(value.split(""));
476
+ set.add("g");
477
+ return [...set].join("");
478
+ }
479
+ function firstChar(value) {
480
+ const trimmed = value?.trim();
481
+ return trimmed && trimmed.length > 0 ? trimmed[0] : "█";
482
+ }
483
+ function applyCompiledMaskRules(line, rules) {
484
+ let out = line;
485
+ for (const rule of rules) out = out.replace(rule.regex, (match) => rule.preserveLength ? rule.replacement.repeat(match.length) : rule.replacement);
486
+ return out;
487
+ }
488
+ //#endregion
489
+ //#region src/util/hash.ts
490
+ function fnv1a32(text) {
491
+ let hash = 2166136261;
492
+ for (let i = 0; i < text.length; i += 1) {
493
+ hash ^= text.charCodeAt(i);
494
+ hash = Math.imul(hash, 16777619);
495
+ }
496
+ return (hash >>> 0).toString(16).padStart(8, "0");
497
+ }
498
+ //#endregion
499
+ //#region src/util/sleep.ts
500
+ async function sleep(ms) {
501
+ await new Promise((resolve) => {
502
+ setTimeout(resolve, ms);
503
+ });
504
+ }
505
+ //#endregion
506
+ //#region src/trace/asciicast.ts
507
+ function encodeAsciicast(header, events) {
508
+ const lines = [JSON.stringify(header)];
509
+ for (const event of events) lines.push(JSON.stringify(event));
510
+ return `${lines.join("\n")}\n`;
511
+ }
512
+ //#endregion
513
+ //#region src/trace/recorder.ts
514
+ const DEFAULT_MAX_EVENTS = 5e4;
515
+ const DEFAULT_MAX_DATA_CHARS = 5e6;
516
+ const DEFAULT_TIME_PRECISION_MS = 1;
517
+ var TraceRecorder = class {
518
+ header;
519
+ startedAtMs;
520
+ maxEvents;
521
+ maxDataChars;
522
+ mergeOutput;
523
+ timePrecisionMs;
524
+ events = [];
525
+ dataChars = 0;
526
+ droppedEvents = 0;
527
+ droppedDataChars = 0;
528
+ constructor(header, options) {
529
+ this.header = header;
530
+ this.startedAtMs = performance.now();
531
+ this.maxEvents = Math.max(1, Math.trunc(options?.maxEvents ?? DEFAULT_MAX_EVENTS));
532
+ this.maxDataChars = Math.max(1, Math.trunc(options?.maxDataChars ?? DEFAULT_MAX_DATA_CHARS));
533
+ this.mergeOutput = options?.mergeOutput ?? true;
534
+ this.timePrecisionMs = Math.max(1, Math.trunc(options?.timePrecisionMs ?? DEFAULT_TIME_PRECISION_MS));
535
+ }
536
+ recordOutput(data) {
537
+ this.addEvent("o", data);
538
+ }
539
+ recordInput(data) {
540
+ this.addEvent("i", data);
541
+ }
542
+ recordResize(cols, rows) {
543
+ this.addEvent("r", `${cols}x${rows}`);
544
+ }
545
+ mark(label) {
546
+ this.addEvent("m", label ?? "");
547
+ }
548
+ snapshot(options) {
549
+ const tailEvents = options?.tailEvents;
550
+ const events = tailEvents ? this.events.slice(-Math.max(0, Math.trunc(tailEvents))) : [...this.events];
551
+ return {
552
+ header: this.header,
553
+ events,
554
+ cast: encodeAsciicast(this.header, events),
555
+ droppedEvents: this.droppedEvents,
556
+ droppedDataChars: this.droppedDataChars
557
+ };
558
+ }
559
+ addEvent(type, data) {
560
+ const timeSeconds = this.nowSeconds();
561
+ const last = this.events.at(-1);
562
+ if (this.mergeOutput && type === "o" && last && last[1] === "o" && last[0] === timeSeconds) {
563
+ last[2] += data;
564
+ this.dataChars += data.length;
565
+ this.trim();
566
+ return;
567
+ }
568
+ this.events.push([
569
+ timeSeconds,
570
+ type,
571
+ data
572
+ ]);
573
+ this.dataChars += data.length;
574
+ this.trim();
575
+ }
576
+ nowSeconds() {
577
+ const elapsedMs = performance.now() - this.startedAtMs;
578
+ return Math.round(elapsedMs / this.timePrecisionMs) * this.timePrecisionMs / 1e3;
579
+ }
580
+ trim() {
581
+ while (this.events.length > this.maxEvents || this.dataChars > this.maxDataChars) {
582
+ const removed = this.events.shift();
583
+ if (!removed) break;
584
+ const chars = removed[2].length;
585
+ this.dataChars -= chars;
586
+ this.droppedEvents += 1;
587
+ this.droppedDataChars += chars;
588
+ }
589
+ }
590
+ };
591
+ //#endregion
592
+ //#region src/session/terminal_session.ts
593
+ const ESC = "\x1B";
594
+ var TerminalSession = class {
595
+ id;
596
+ pty;
597
+ terminal;
598
+ snapshotRingSize;
599
+ trace;
600
+ writeChain = Promise.resolve();
601
+ disposables = [];
602
+ rawOutputRing = [];
603
+ snapshotRing = [];
604
+ closed = null;
605
+ constructor(options) {
606
+ this.id = options.id;
607
+ this.pty = options.pty;
608
+ this.snapshotRingSize = options.snapshotRingSize;
609
+ this.terminal = new Terminal({
610
+ cols: options.cols,
611
+ rows: options.rows,
612
+ allowProposedApi: true,
613
+ scrollback: 2e3,
614
+ convertEol: true
615
+ });
616
+ this.trace = new TraceRecorder({
617
+ version: 2,
618
+ width: options.cols,
619
+ height: options.rows,
620
+ timestamp: Math.floor(Date.now() / 1e3),
621
+ env: options.trace?.env,
622
+ title: options.trace?.title ?? options.id,
623
+ command: options.trace?.command,
624
+ term: options.trace?.env?.TERM
625
+ });
626
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => this.handleCsiDsr(params)));
627
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => this.handleCsiDa(params)));
628
+ this.disposables.push(this.pty.onData((data) => {
629
+ this.appendRawOutput(data);
630
+ this.trace.recordOutput(data);
631
+ this.enqueueWrite(data);
632
+ }));
633
+ this.disposables.push(this.pty.onExit((event) => {
634
+ this.closed = {
635
+ type: "process_exit",
636
+ exitCode: event.exitCode,
637
+ signal: event.signal
638
+ };
639
+ }));
640
+ }
641
+ get cols() {
642
+ return this.terminal.cols;
643
+ }
644
+ get rows() {
645
+ return this.terminal.rows;
646
+ }
647
+ isClosed() {
648
+ return this.closed !== null;
649
+ }
650
+ getCloseReason() {
651
+ return this.closed;
652
+ }
653
+ resize(cols, rows) {
654
+ this.trace.recordResize(cols, rows);
655
+ this.pty.resize(cols, rows);
656
+ this.terminal.resize(cols, rows);
657
+ }
658
+ sendText(text, options) {
659
+ const payload = options?.enter ?? false ? `${text}\r` : text;
660
+ this.trace.recordInput(payload);
661
+ this.pty.write(payload);
662
+ }
663
+ pressKey(key) {
664
+ const encoded = encodeKey(key);
665
+ this.trace.recordInput(encoded);
666
+ this.pty.write(encoded);
667
+ }
668
+ sendMouse(event) {
669
+ const encoded = encodeSgrMouse(event);
670
+ this.trace.recordInput(encoded);
671
+ this.pty.write(encoded);
672
+ }
673
+ async flush() {
674
+ await this.writeChain;
675
+ }
676
+ getMeta() {
677
+ const buffer = this.terminal.buffer.active;
678
+ return {
679
+ cols: this.terminal.cols,
680
+ rows: this.terminal.rows,
681
+ bufferType: buffer.type,
682
+ viewportY: buffer.viewportY,
683
+ baseY: buffer.baseY,
684
+ length: buffer.length,
685
+ cursorX: buffer.cursorX,
686
+ cursorY: buffer.cursorY
687
+ };
688
+ }
689
+ async snapshotText(options) {
690
+ await this.flush();
691
+ if (options?.maxLines !== void 0 && options.tailLines !== void 0) throw new Error("snapshotText: maxLines and tailLines are mutually exclusive");
692
+ let lines = snapshotLines(this.terminal, {
693
+ scope: options?.scope,
694
+ trimRight: options?.trimRight
695
+ });
696
+ if (options?.trimBottom ?? true) lines = trimBottomEmptyLines(lines);
697
+ if (options?.maxLines !== void 0) {
698
+ const max = Math.max(0, Math.trunc(options.maxLines));
699
+ lines = lines.slice(0, max);
700
+ }
701
+ if (options?.tailLines !== void 0) {
702
+ const tail = Math.max(0, Math.trunc(options.tailLines));
703
+ lines = lines.slice(Math.max(0, lines.length - tail));
704
+ }
705
+ lines = applyTextMaskRules(lines, options?.mask);
706
+ const text = lines.join("\n");
707
+ const hash = fnv1a32(text);
708
+ if (options?.captureFrame ?? true) this.captureFrame(text, hash);
709
+ return {
710
+ text,
711
+ hash
712
+ };
713
+ }
714
+ async snapshotAnsi(options) {
715
+ await this.flush();
716
+ if (options?.maxLines !== void 0 && options.tailLines !== void 0) throw new Error("snapshotAnsi: maxLines and tailLines are mutually exclusive");
717
+ let lines = renderAnsiLines(this.terminal, {
718
+ scope: options?.scope,
719
+ trimRight: options?.trimRight
720
+ });
721
+ if (options?.trimBottom ?? true) lines = trimBottomEmptyAnsiLines(lines);
722
+ if (options?.maxLines !== void 0) {
723
+ const max = Math.max(0, Math.trunc(options.maxLines));
724
+ lines = lines.slice(0, max);
725
+ }
726
+ if (options?.tailLines !== void 0) {
727
+ const tail = Math.max(0, Math.trunc(options.tailLines));
728
+ lines = lines.slice(Math.max(0, lines.length - tail));
729
+ }
730
+ if (options?.mask && options.mask.length > 0) {
731
+ const maskedPlain = applyTextMaskRules(lines.map((line) => line.plain), options.mask);
732
+ const maskedAnsi = applyTextMaskRules(lines.map((line) => line.ansi), options.mask);
733
+ lines = lines.map((line, idx) => ({
734
+ ...line,
735
+ plain: maskedPlain[idx] ?? "",
736
+ ansi: maskedAnsi[idx] ?? ""
737
+ }));
738
+ }
739
+ const ansi = lines.map((l) => l.ansi).join("\n");
740
+ return {
741
+ ansi,
742
+ plain: lines.map((l) => l.plain).join("\n"),
743
+ hash: fnv1a32(ansi),
744
+ lines
745
+ };
746
+ }
747
+ async snapshotGrid(options) {
748
+ await this.flush();
749
+ const grid = snapshotGrid(this.terminal, {
750
+ trimRight: options?.trimRight,
751
+ includeStyles: options?.includeStyles
752
+ });
753
+ const hash = fnv1a32(JSON.stringify(grid));
754
+ if (options?.captureFrame ?? true) this.captureFrame(grid.lines.join("\n"), hash);
755
+ return {
756
+ grid,
757
+ hash
758
+ };
759
+ }
760
+ async snapshotCast(options) {
761
+ await this.flush();
762
+ return this.trace.snapshot({ tailEvents: options?.tailEvents });
763
+ }
764
+ mark(label) {
765
+ this.trace.mark(label);
766
+ }
767
+ getSnapshotFrames() {
768
+ return [...this.snapshotRing];
769
+ }
770
+ getRawOutputChunks() {
771
+ return [...this.rawOutputRing];
772
+ }
773
+ async waitForText(args) {
774
+ const startedAt = Date.now();
775
+ let closedSince = null;
776
+ while (Date.now() - startedAt <= args.timeoutMs) {
777
+ const snapshot = await this.snapshotText({
778
+ captureFrame: true,
779
+ scope: args.scope
780
+ });
781
+ if (args.text && snapshot.text.includes(args.text)) return {
782
+ found: true,
783
+ ...snapshot
784
+ };
785
+ if (args.regex && args.regex.test(snapshot.text)) return {
786
+ found: true,
787
+ ...snapshot
788
+ };
789
+ if (this.isClosed()) {
790
+ closedSince ??= Date.now();
791
+ const drainMs = Math.max(500, args.intervalMs * 4);
792
+ if (Date.now() - closedSince >= drainMs) break;
793
+ }
794
+ await sleep(args.intervalMs);
795
+ }
796
+ return {
797
+ found: false,
798
+ ...await this.snapshotText({
799
+ captureFrame: true,
800
+ scope: args.scope
801
+ })
802
+ };
803
+ }
804
+ async waitForStableScreen(args) {
805
+ const startedAt = Date.now();
806
+ let stableSince = null;
807
+ let lastHash = null;
808
+ while (Date.now() - startedAt <= args.timeoutMs) {
809
+ const snapshot = await this.snapshotText({ captureFrame: true });
810
+ if (snapshot.hash === lastHash) stableSince ??= Date.now();
811
+ else {
812
+ stableSince = null;
813
+ lastHash = snapshot.hash;
814
+ }
815
+ if (stableSince !== null && Date.now() - stableSince >= args.quietMs) return {
816
+ stable: true,
817
+ ...snapshot
818
+ };
819
+ await sleep(args.intervalMs);
820
+ }
821
+ return {
822
+ stable: false,
823
+ ...await this.snapshotText({ captureFrame: true })
824
+ };
825
+ }
826
+ close() {
827
+ if (this.closed === null) this.closed = { type: "closed_by_user" };
828
+ this.pty.kill();
829
+ for (const d of this.disposables) d.dispose();
830
+ this.terminal.dispose();
831
+ }
832
+ enqueueWrite(data) {
833
+ this.writeChain = this.writeChain.then(() => new Promise((resolve) => {
834
+ this.terminal.write(data, resolve);
835
+ }));
836
+ }
837
+ appendRawOutput(data) {
838
+ this.rawOutputRing.push(data);
839
+ if (this.rawOutputRing.length > 2e3) this.rawOutputRing.splice(0, this.rawOutputRing.length - 2e3);
840
+ }
841
+ captureFrame(text, hash) {
842
+ this.snapshotRing.push({
843
+ atMs: Date.now(),
844
+ hash,
845
+ text
846
+ });
847
+ if (this.snapshotRing.length > this.snapshotRingSize) this.snapshotRing.splice(0, this.snapshotRing.length - this.snapshotRingSize);
848
+ }
849
+ writePtySafely(data) {
850
+ if (this.isClosed()) return;
851
+ try {
852
+ this.pty.write(data);
853
+ } catch {}
854
+ }
855
+ handleCsiDsr(params) {
856
+ if (params.length !== 1) return false;
857
+ const raw = params[0];
858
+ const value = Array.isArray(raw) ? raw[0] : raw;
859
+ if (value === 5) {
860
+ this.writePtySafely(`${ESC}[0n`);
861
+ return true;
862
+ }
863
+ if (value !== 6) return false;
864
+ const meta = this.getMeta();
865
+ const row = meta.baseY + meta.cursorY - meta.viewportY + 1;
866
+ const col = meta.cursorX + 1;
867
+ this.writePtySafely(`${ESC}[${row};${col}R`);
868
+ return true;
869
+ }
870
+ handleCsiDa(params) {
871
+ if (params.length > 1) return false;
872
+ const raw = params[0];
873
+ if ((raw === void 0 ? 0 : Array.isArray(raw) ? raw[0] : raw) !== 0) return false;
874
+ this.writePtySafely(`${ESC}[?1;2c`);
875
+ return true;
876
+ }
877
+ };
878
+ function trimBottomEmptyLines(lines) {
879
+ let end = lines.length;
880
+ while (end > 0 && lines[end - 1] === "") end -= 1;
881
+ return end === lines.length ? lines : lines.slice(0, end);
882
+ }
883
+ function trimBottomEmptyAnsiLines(lines) {
884
+ let end = lines.length;
885
+ while (end > 0) {
886
+ const line = lines[end - 1];
887
+ if (!(!line?.hasStyle && (line?.plain ?? "").trim() === "")) break;
888
+ end -= 1;
889
+ }
890
+ return end === lines.length ? lines : lines.slice(0, end);
891
+ }
892
+ //#endregion
893
+ export { applyTextMaskRules as a, encodeSgrMouse as c, isDefaultStyle as d, styleKey as f, fnv1a32 as i, extractStyle as l, TraceRecorder as n, snapshotGrid as o, encodeKey as p, sleep as r, snapshotLines as s, TerminalSession as t, findMeaningfulEndCol as u };