ptywright 0.1.1 → 0.3.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 (67) hide show
  1. package/README.md +318 -1
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-CfvlbRoZ.mjs +3585 -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-zApMYWZx.mjs +3257 -0
  11. package/dist/runner-zi0nItvB.mjs +1874 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-BC3yo-dq.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 +166 -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/bin/ptywright +0 -4
  29. package/src/cli.ts +0 -414
  30. package/src/generator/doc_parser.ts +0 -341
  31. package/src/generator/generate.ts +0 -161
  32. package/src/generator/index.ts +0 -10
  33. package/src/generator/script_generator.ts +0 -209
  34. package/src/generator/step_extractor.ts +0 -397
  35. package/src/mcp/http_server.ts +0 -174
  36. package/src/mcp/script_recording.ts +0 -238
  37. package/src/mcp/server.ts +0 -1348
  38. package/src/pty/bun_pty_adapter.ts +0 -34
  39. package/src/pty/bun_terminal_adapter.ts +0 -149
  40. package/src/pty/pty_adapter.ts +0 -31
  41. package/src/script/dsl.ts +0 -188
  42. package/src/script/module.ts +0 -43
  43. package/src/script/path.ts +0 -151
  44. package/src/script/run.ts +0 -108
  45. package/src/script/run_all.ts +0 -229
  46. package/src/script/runner.ts +0 -983
  47. package/src/script/schema.ts +0 -237
  48. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  49. package/src/script/steps/index.ts +0 -2
  50. package/src/script/suite_report.ts +0 -626
  51. package/src/session/session_manager.ts +0 -145
  52. package/src/session/terminal_session.ts +0 -473
  53. package/src/terminal/ansi.ts +0 -142
  54. package/src/terminal/keys.ts +0 -180
  55. package/src/terminal/mask.ts +0 -70
  56. package/src/terminal/mouse.ts +0 -75
  57. package/src/terminal/snapshot.ts +0 -196
  58. package/src/terminal/style.ts +0 -121
  59. package/src/terminal/view.ts +0 -49
  60. package/src/trace/asciicast.ts +0 -20
  61. package/src/trace/asciinema_player_assets.ts +0 -44
  62. package/src/trace/cast_to_txt.ts +0 -116
  63. package/src/trace/recorder.ts +0 -110
  64. package/src/trace/report.ts +0 -2092
  65. package/src/types.ts +0 -86
  66. package/src/util/hash.ts +0 -8
  67. package/src/util/sleep.ts +0 -5
@@ -1,121 +0,0 @@
1
- import type { Terminal } from "@xterm/headless";
2
-
3
- export type Color =
4
- | { mode: "default" }
5
- | { mode: "palette"; value: number }
6
- | { mode: "rgb"; value: number };
7
-
8
- export type CellStyle = {
9
- fg: Color;
10
- bg: Color;
11
- bold: boolean;
12
- dim: boolean;
13
- italic: boolean;
14
- underline: boolean;
15
- inverse: boolean;
16
- strikethrough: boolean;
17
- };
18
-
19
- export const DEFAULT_STYLE: CellStyle = {
20
- fg: { mode: "default" },
21
- bg: { mode: "default" },
22
- bold: false,
23
- dim: false,
24
- italic: false,
25
- underline: false,
26
- inverse: false,
27
- strikethrough: false,
28
- };
29
-
30
- export function extractStyle(
31
- cell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
32
- ): CellStyle {
33
- const fg = extractColor(
34
- cell.isFgDefault(),
35
- cell.isFgPalette(),
36
- cell.isFgRGB(),
37
- cell.getFgColor(),
38
- );
39
- const bg = extractColor(
40
- cell.isBgDefault(),
41
- cell.isBgPalette(),
42
- cell.isBgRGB(),
43
- cell.getBgColor(),
44
- );
45
-
46
- const style: CellStyle = {
47
- fg,
48
- bg,
49
- bold: cell.isBold() !== 0,
50
- dim: cell.isDim() !== 0,
51
- italic: cell.isItalic() !== 0,
52
- underline: cell.isUnderline() !== 0,
53
- inverse: cell.isInverse() !== 0,
54
- strikethrough: cell.isStrikethrough() !== 0,
55
- };
56
-
57
- return isDefaultStyle(style) ? DEFAULT_STYLE : style;
58
- }
59
-
60
- export function isDefaultStyle(style: CellStyle): boolean {
61
- return (
62
- style.fg.mode === "default" &&
63
- style.bg.mode === "default" &&
64
- !style.bold &&
65
- !style.dim &&
66
- !style.italic &&
67
- !style.underline &&
68
- !style.inverse &&
69
- !style.strikethrough
70
- );
71
- }
72
-
73
- export function styleKey(style: CellStyle): string {
74
- const fg = style.fg.mode === "default" ? "d" : `${style.fg.mode}:${style.fg.value}`;
75
- const bg = style.bg.mode === "default" ? "d" : `${style.bg.mode}:${style.bg.value}`;
76
- return [
77
- fg,
78
- bg,
79
- style.bold ? "b" : "",
80
- style.dim ? "d" : "",
81
- style.italic ? "i" : "",
82
- style.underline ? "u" : "",
83
- style.inverse ? "r" : "",
84
- style.strikethrough ? "s" : "",
85
- ].join("|");
86
- }
87
-
88
- export function findMeaningfulEndCol(
89
- line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
90
- cols: number,
91
- nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
92
- ): number {
93
- if (!line) return 0;
94
-
95
- for (let x = cols - 1; x >= 0; x -= 1) {
96
- const cell = line.getCell(x, nullCell);
97
- if (!cell) continue;
98
- if (cell.getWidth() === 0) continue;
99
-
100
- const chars = cell.getChars();
101
- const style = extractStyle(cell);
102
- const meaningful = (chars !== "" && chars !== " ") || !isDefaultStyle(style);
103
- if (meaningful) {
104
- return x + 1;
105
- }
106
- }
107
-
108
- return 0;
109
- }
110
-
111
- function extractColor(
112
- isDefault: boolean,
113
- isPalette: boolean,
114
- isRgb: boolean,
115
- value: number,
116
- ): Color {
117
- if (isDefault) return { mode: "default" };
118
- if (isRgb) return { mode: "rgb", value };
119
- if (isPalette) return { mode: "palette", value };
120
- return { mode: "default" };
121
- }
@@ -1,49 +0,0 @@
1
- import type { SnapshotScope } from "./snapshot";
2
-
3
- export type TerminalMeta = {
4
- cols: number;
5
- rows: number;
6
- bufferType: "normal" | "alternate";
7
- viewportY: number;
8
- baseY: number;
9
- length: number;
10
- cursorX: number;
11
- cursorY: number;
12
- };
13
-
14
- export type SnapshotViewOptions = {
15
- sessionId: string;
16
- scope: SnapshotScope;
17
- hash: string;
18
- lines: string[];
19
- meta: TerminalMeta;
20
- lineNumbers?: boolean;
21
- };
22
-
23
- export function formatSnapshotView(options: SnapshotViewOptions): string {
24
- const lineNumbers = options.lineNumbers ?? true;
25
-
26
- const cursorAbsY = options.meta.baseY + options.meta.cursorY;
27
- const cursorViewportRow = cursorAbsY - options.meta.viewportY;
28
- const cursorViewportCol = options.meta.cursorX;
29
-
30
- const header = [
31
- `session=${options.sessionId}`,
32
- `scope=${options.scope}`,
33
- `size=${options.meta.cols}x${options.meta.rows}`,
34
- `buffer=${options.meta.bufferType}`,
35
- `cursor=${cursorViewportCol + 1},${cursorViewportRow + 1}`,
36
- `hash=${options.hash}`,
37
- ].join(" ");
38
-
39
- const digits = Math.max(2, String(options.lines.length).length);
40
- const out: string[] = [header];
41
-
42
- for (let i = 0; i < options.lines.length; i += 1) {
43
- const n = i + 1;
44
- const prefix = lineNumbers ? `${String(n).padStart(digits, "0")}│ ` : "";
45
- out.push(`${prefix}${options.lines[i] ?? ""}`);
46
- }
47
-
48
- return out.join("\n");
49
- }
@@ -1,20 +0,0 @@
1
- export type AsciicastHeader = {
2
- version: 2;
3
- width: number;
4
- height: number;
5
- timestamp?: number;
6
- env?: Record<string, string>;
7
- title?: string;
8
- command?: string;
9
- term?: string;
10
- };
11
-
12
- export type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
13
-
14
- export function encodeAsciicast(header: AsciicastHeader, events: AsciicastEvent[]): string {
15
- const lines: string[] = [JSON.stringify(header)];
16
- for (const event of events) {
17
- lines.push(JSON.stringify(event));
18
- }
19
- return `${lines.join("\n")}\n`;
20
- }
@@ -1,44 +0,0 @@
1
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { createRequire } from "node:module";
4
-
5
- export type EnsureAsciinemaPlayerAssetsResult = {
6
- ok: boolean;
7
- copied: boolean;
8
- cssPath: string;
9
- jsPath: string;
10
- error?: string;
11
- };
12
-
13
- export function ensureAsciinemaPlayerAssets(reportPath: string): EnsureAsciinemaPlayerAssetsResult {
14
- const dir = dirname(reportPath);
15
- const cssPath = join(dir, "asciinema-player.css");
16
- const jsPath = join(dir, "asciinema-player.min.js");
17
-
18
- const cssExists = existsSync(cssPath);
19
- const jsExists = existsSync(jsPath);
20
- if (cssExists && jsExists) {
21
- return { ok: true, copied: false, cssPath, jsPath };
22
- }
23
-
24
- try {
25
- mkdirSync(dir, { recursive: true });
26
-
27
- const require = createRequire(import.meta.url);
28
- const resolvedCss = require.resolve("asciinema-player/dist/bundle/asciinema-player.css");
29
- const resolvedJs = require.resolve("asciinema-player/dist/bundle/asciinema-player.min.js");
30
-
31
- if (!cssExists) copyFileSync(resolvedCss, cssPath);
32
- if (!jsExists) copyFileSync(resolvedJs, jsPath);
33
-
34
- return { ok: true, copied: true, cssPath, jsPath };
35
- } catch (error) {
36
- return {
37
- ok: false,
38
- copied: false,
39
- cssPath,
40
- jsPath,
41
- error: (error as Error).message,
42
- };
43
- }
44
- }
@@ -1,116 +0,0 @@
1
- function parseArgs(argv: string[]): { inPath: string; outPath?: string; stripAnsi: boolean } {
2
- const out: Partial<{ inPath: string; outPath?: string; stripAnsi: boolean }> = {
3
- stripAnsi: true,
4
- };
5
-
6
- for (let i = 0; i < argv.length; i += 1) {
7
- const arg = argv[i];
8
- const next = argv[i + 1];
9
-
10
- if (!out.inPath && arg && !arg.startsWith("-")) {
11
- out.inPath = arg;
12
- continue;
13
- }
14
-
15
- if (arg === "--in" && next) {
16
- out.inPath = next;
17
- i += 1;
18
- continue;
19
- }
20
-
21
- if (arg === "--out" && next) {
22
- out.outPath = next;
23
- i += 1;
24
- continue;
25
- }
26
-
27
- if (arg === "--strip-ansi") {
28
- out.stripAnsi = true;
29
- continue;
30
- }
31
-
32
- if (arg === "--keep-ansi") {
33
- out.stripAnsi = false;
34
- continue;
35
- }
36
-
37
- throw new Error(`unknown arg: ${arg ?? ""}`);
38
- }
39
-
40
- if (!out.inPath) throw new Error("missing <castPath> (or --in <path>)");
41
- return out as { inPath: string; outPath?: string; stripAnsi: boolean };
42
- }
43
-
44
- type AsciicastHeader = {
45
- version?: number;
46
- };
47
-
48
- type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
49
-
50
- function stripAnsi(text: string): string {
51
- // CSI: ESC [ ... @-~ (includes SGR, cursor, erase, etc)
52
- // eslint-disable-next-line no-control-regex
53
- const csi = /\u001b\[[0-?]*[ -/]*[@-~]/g;
54
- // OSC: ESC ] ... BEL or ST (ESC \)
55
- // eslint-disable-next-line no-control-regex
56
- const osc = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
57
- // Two-byte escapes: SS3 (ESC O) + final
58
- // eslint-disable-next-line no-control-regex
59
- const ss3 = /\u001bO[@-~]/g;
60
-
61
- return text.replace(osc, "").replace(csi, "").replace(ss3, "");
62
- }
63
-
64
- async function run(args: { inPath: string; outPath?: string; stripAnsi: boolean }): Promise<void> {
65
- const input = await Bun.file(args.inPath).text();
66
- const lines = input.split("\n").filter((l) => l.length > 0);
67
- if (lines.length === 0) throw new Error("empty cast");
68
-
69
- let header: AsciicastHeader | null = null;
70
- try {
71
- header = JSON.parse(lines[0] ?? "") as AsciicastHeader;
72
- } catch {
73
- throw new Error("invalid cast header JSON");
74
- }
75
-
76
- if (header?.version !== 2) {
77
- throw new Error(`unsupported asciicast version: ${header?.version ?? "unknown"}`);
78
- }
79
-
80
- let out = "";
81
- for (const line of lines.slice(1)) {
82
- let parsed: unknown;
83
- try {
84
- parsed = JSON.parse(line);
85
- } catch {
86
- continue;
87
- }
88
-
89
- if (!Array.isArray(parsed) || parsed.length < 3) continue;
90
- const event = parsed as Partial<AsciicastEvent>;
91
- if (event[1] !== "o") continue;
92
- if (typeof event[2] !== "string") continue;
93
-
94
- out += event[2];
95
- }
96
-
97
- const rendered = args.stripAnsi ? stripAnsi(out) : out;
98
-
99
- if (args.outPath) {
100
- await Bun.write(args.outPath, rendered);
101
- } else {
102
- process.stdout.write(rendered);
103
- if (!rendered.endsWith("\n")) process.stdout.write("\n");
104
- }
105
- }
106
-
107
- if (import.meta.main) {
108
- try {
109
- const args = parseArgs(process.argv.slice(2));
110
- await run(args);
111
- } catch (error) {
112
- // eslint-disable-next-line no-console
113
- console.error((error as Error).message);
114
- process.exitCode = 1;
115
- }
116
- }
@@ -1,110 +0,0 @@
1
- import type { AsciicastEvent, AsciicastHeader } from "./asciicast";
2
- import { encodeAsciicast } from "./asciicast";
3
-
4
- export type TraceRecorderOptions = {
5
- maxEvents?: number;
6
- maxDataChars?: number;
7
- mergeOutput?: boolean;
8
- timePrecisionMs?: number;
9
- };
10
-
11
- export type TraceSnapshot = {
12
- header: AsciicastHeader;
13
- events: AsciicastEvent[];
14
- cast: string;
15
- droppedEvents: number;
16
- droppedDataChars: number;
17
- };
18
-
19
- const DEFAULT_MAX_EVENTS = 50_000;
20
- const DEFAULT_MAX_DATA_CHARS = 5_000_000;
21
- const DEFAULT_TIME_PRECISION_MS = 1;
22
-
23
- export class TraceRecorder {
24
- private readonly header: AsciicastHeader;
25
- private readonly startedAtMs: number;
26
- private readonly maxEvents: number;
27
- private readonly maxDataChars: number;
28
- private readonly mergeOutput: boolean;
29
- private readonly timePrecisionMs: number;
30
-
31
- private readonly events: AsciicastEvent[] = [];
32
- private dataChars = 0;
33
- private droppedEvents = 0;
34
- private droppedDataChars = 0;
35
-
36
- constructor(header: AsciicastHeader, options?: TraceRecorderOptions) {
37
- this.header = header;
38
- this.startedAtMs = performance.now();
39
- this.maxEvents = Math.max(1, Math.trunc(options?.maxEvents ?? DEFAULT_MAX_EVENTS));
40
- this.maxDataChars = Math.max(1, Math.trunc(options?.maxDataChars ?? DEFAULT_MAX_DATA_CHARS));
41
- this.mergeOutput = options?.mergeOutput ?? true;
42
- this.timePrecisionMs = Math.max(
43
- 1,
44
- Math.trunc(options?.timePrecisionMs ?? DEFAULT_TIME_PRECISION_MS),
45
- );
46
- }
47
-
48
- recordOutput(data: string): void {
49
- this.addEvent("o", data);
50
- }
51
-
52
- recordInput(data: string): void {
53
- this.addEvent("i", data);
54
- }
55
-
56
- recordResize(cols: number, rows: number): void {
57
- this.addEvent("r", `${cols}x${rows}`);
58
- }
59
-
60
- mark(label?: string): void {
61
- this.addEvent("m", label ?? "");
62
- }
63
-
64
- snapshot(options?: { tailEvents?: number }): TraceSnapshot {
65
- const tailEvents = options?.tailEvents;
66
- const events = tailEvents
67
- ? this.events.slice(-Math.max(0, Math.trunc(tailEvents)))
68
- : [...this.events];
69
- return {
70
- header: this.header,
71
- events,
72
- cast: encodeAsciicast(this.header, events),
73
- droppedEvents: this.droppedEvents,
74
- droppedDataChars: this.droppedDataChars,
75
- };
76
- }
77
-
78
- private addEvent(type: AsciicastEvent[1], data: string): void {
79
- const timeSeconds = this.nowSeconds();
80
-
81
- const last = this.events.at(-1);
82
- if (this.mergeOutput && type === "o" && last && last[1] === "o" && last[0] === timeSeconds) {
83
- last[2] += data;
84
- this.dataChars += data.length;
85
- this.trim();
86
- return;
87
- }
88
-
89
- this.events.push([timeSeconds, type, data]);
90
- this.dataChars += data.length;
91
- this.trim();
92
- }
93
-
94
- private nowSeconds(): number {
95
- const elapsedMs = performance.now() - this.startedAtMs;
96
- const quantized = Math.round(elapsedMs / this.timePrecisionMs) * this.timePrecisionMs;
97
- return quantized / 1000;
98
- }
99
-
100
- private trim(): void {
101
- while (this.events.length > this.maxEvents || this.dataChars > this.maxDataChars) {
102
- const removed = this.events.shift();
103
- if (!removed) break;
104
- const chars = removed[2].length;
105
- this.dataChars -= chars;
106
- this.droppedEvents += 1;
107
- this.droppedDataChars += chars;
108
- }
109
- }
110
- }